I have 2000+ developers at my work, many of them making changes on a single huge web application. Compile/publish of the testing servers takes several hours and happens regularly throughout the day. There are thousands of testers all testing many different workflows all the time.
If one developer makes a mistake in a dark corner of the code one day, it seems bad to have the whole server crashing and disrupting all the other testing that’s going on for multiple hours while we wait for a fix and another build.
Quality and fix prioritization is in the hands of project teams for usability problems, performance problems, functional problems, etc. Uncaught exceptions should be logged somewhere, and it’s ultimately up to the team to decide whether they care enough to fix them or not.
You do not need to solve that at the language level. A common pattern is using multiple replicas, service discovery and automatic restart upon a dead replica. For example kubernetes does this out of the box.
For dark corners of the code, that is often a good middle ground between interrupting the service and never die.
A crash in a web server could result in data loss, timeouts and performance issues on clients due to the response never coming back, crashes on poorly written clients, cascading failures in poorly written services, etc.
And for a client application, imagine if your web browser would just randomly crash sometimes. Do you think it would make it much better if the browser would detect the crash and relaunch itself? And what if it relaunches itself and then crashes again for the same reason?
- Array.get/Array.get_mut for when you want to panic on an out-of-bounds index
- Array.opt/Array.opt_mut for when you want an Option type, with it being a None for an out of bounds index
So for example:
[10, 20, 30].get(0) => 10
[10, 20, 30].get(42) => panic
[10, 20, 30].opt(0) => Option.Some(10)
[10, 20, 30].opt(42) => Option.None
This pattern is applied across the standard library where this makes sense, such that the developer can pick which option works best for their needs.There are a few places where we check for some input at runtime and panic if this is invalid, but this is limited to cases where there's simply no sensible alternative (e.g. providing a key with an invalid size to a ChaCha20 cipher).
That's not scalable if it's not possible to catch panics. If panics can't be handled they must only be caused by super corner 0.001% cases, not by fundamental things like dividing an integer or indexing an array.
In an open source library that I maintain, the C code makes use of asserts. These asserts are essentially unrecoverable panics on constraint/invariant violation. The effect of an assert failure is that violation renders the code unrunnable ("utterly broken"). Unfortunately "panic, go fix it" turns out to not be a great fit for a widely distributed library for at least three reasons: (1) the user of a library is often not in a position to fix it, (2) the distribution is so far removed from the maintainers that the maintainers may or may not get the message to fix it, and even if they can fix it the turnaround time to the next distro release doesn't help the current user very much, (3) if the constraint violations can be triggered by external code then we are essentially introducing "transitive panic" for conditions that we can't possibly fix (we don't control or have visibility into external code, such as Windows APIs and drivers).
The upshot of this is that our policies are to only panic (i.e. use an assert) to check conditions/constraints/invariants that are under the full control of our own code base (never to confirm assumptions about externally supplied data). If we are in a context where it is practicable, prefer raising error conditions such as "internal error" or "unexpected native API response".
With the above in mind, I disagree with your point about supervision trees and restarting lightweight processes. Requiring panics to crash the whole process is the most extreme outcome possible, and can potentially lead to an extremely unsatisfactory user experience. Software is almost always performing many more functions than the little piece of code that panics. I find it difficult to believe that killing the whole process (i.e. unrecoverable panic) is universally the best course of action. Killing and permanently disabling the lightweight process would at least allow the rest of the program a chance to continue in some kind of limp mode until the issue can be fixed.
Looking at it another way, by killing the whole process, you are effectively limiting use to those cases where killing the whole process is indeed the only appropriate course of action.
On "you'll keep running into the issue over and over again and possibly even end up ignoring it" To me this smells over-opinionated and poorly justified. So long as there is good error reporting and diagnostics tracing, the user of the language can and should decide these things. But maybe it can be better justified and maybe there are plenty of use-cases that are fine with "no soup for you!" at the first sign of trouble.
Sorry, but limping along is better than crashing.
For runtime error handling you'd use algebraic types such as Result and Option, similar to most functional languages and Rust.
The reason we don't allow catching of panics (such as Rust allows) is because it can result in people more or less ignoring such errors (something I've seen happen far too often in past projects) and just retrying over and over again, achieving the same result every time. I'd much rather have the program crash and scream loudly, forcing developers to address the issue. Fortunately, the chances of you actually running into a panic at runtime should be pretty slim, so not being able to catch them shouldn't be much of an issue in practice.