Rust has featured unwinding in some form or another since its inception.
It has never been the position of the Rust developers that "exceptions suck", only that the failure mode of code that is not exception-safe is especially severe for low-level languages, especially ones that intend to be memory-safe. Rust's aversion to exceptions is not intended as a condemnation of their use in languages that make different tradeoffs.
The only way to "catch" a panic in Rust is via an obscure stdlib API whose purpose is to prevent unwinding from crossing FFI boundaries when Rust code is called from another language (because most host languages consider unwinding across FFI to be undefined behavior). I have read a lot of Rust code; never have I seen anyone attempt to abuse this mechanism as a replacement for general exception handling (which wouldn't even work in general, because Rust code can (and often is) compiled without the ability to unwind at all).
I implore you to actually make an attempt to learn Rust, so that in the future you may be more informed when commenting about it.
In your categorization, Rust with "panic=abort" would be at step 2, not 5 (and it's used in production that way, for instance AFAIK Firefox uses "panic=abort"). And interestingly, not unwinding on panic in Rust is newer than unwinding on panic: it was added only on Rust 1.10 (https://blog.rust-lang.org/2016/07/07/Rust-1.10.html). So Rust seems to be following your list of steps somewhat backwards.
Anger. Ugh, we've got manual error-handling code everywhere. It's awful!
Bargaining. What if we just add a couple of really limited features to handle really exceptional errors...
Depression. Oh, God, now we have all of the trouble of exceptions along with our previous weird half-baked error-handling mechanism.
Acceptance. Well, we've got too many users, so now we're stuck with it. At least people seem to be able to muddle through anyway.
Exceptions are optimized for unwinding (i.e. abort, not handling); error values are optimized for handling; while monadic Maybe / Either constructions try to have it both ways, and are approximately equivalent to checked exceptions with more cumbersome syntax, depending on how good the language is with lambdas. The heavyweight syntax reminds you of the error's existence.
The thing is, while some errors conditions can easily be put in the exceptional category (panic / abort), and others can easily be put in the "you really should handle this, it's not unusual" category, there's a large space in the middle where the preferred choice is heavily dependent on context and the program being written.
Business code called from a server loop almost never cares about exceptions; unwind all the way to the top, rolling back transactions along the way, log, and loop back around to handle the next request. Networking code in a client or server almost always wants to handle errors from the network; network errors are not unusual.
It's almost like the choice should be up to the user at the call site. Unfortunately the decision needs to be global, because error propagate. Errors are peculiar like this: they violate abstraction boundaries. The failure modes of a module are a function of how it's implemented. If the module interface is designed to be polymorphic, wrapping up error conditions in abstract values makes them almost unhandleable - hiding the cause means you can't take action. Thus errors are a leading cause of leaky abstractions.
Erlang was not originally designed for concurrency; it was designed for fault tolerance. That resilience was achieved not by handling errors, but by tearing down the faulting process, and letting a supervisor set things back up again. Killing the process also means throwing away all the data it had in flight, and because data isn't shared, the risk of corruption is much lower, and semantically the tear down looks a lot like a transaction rolled back.
Erlang appears to be pretty good at achieving its goal of fault tolerance. Does it look more like error codes or exception handling? A lot more like exception handling if you ask me. But is all the rest of it needed to follow through properly?
I had to implement part of the unwinding, so I'd like to think I know what I'm talking about.
Errors are handled the long and boring way inline and I like it that way. (I would like better tools for making better errors, but not for pretending errors don't exist.) When I work in C, I do the same.
Your charactization of Go as I know it is wrong.
My gut feeling would be that otherwise, instead of handling errors locally, you “sprinkle” your code with “try” keywords. Afterwards you want to distinguish between exception types and eventually you end up switching over these types in a big switch higher up.
1 - Exceptions do suck. It's obvious in Haskell where there are better tools but sometimes you aren't allowed to use them.
2 - Rust thus goes into: well, let's take those better mechanisms and allow them everywhere. Go goes into: well, let's create some alternative mechanism like the ones most people are used to.
3 - Rust then goes into: oops, our error handling does not stack well so people are just panicking everywhere (what actually requires more code than proper handling). Go goes into: oops our error handling requires 2 times as much code as the actual program, so people are just panicking everywhere.
4 - Rust will eventually solve its crisis by creating something equivalent to "do" notation. I don't really think there's any solution for Go, but I do expect them to create some low level exception system so they can keep the language simple - maybe this time they'll copy VB instead of C.
By the subject of your point #6, Rust's mechanism is strictly more powerful than exceptions. It can do anything exceptions do, and more. Go's evidently is not.
1. I'll just handle all of the errors by having my functions return Result<T, E> where E is some type that encapsulates all of different error conditions that can happen in my function.
2. Function one returns Result<T1, E1> and function two returns Result<T2, E2>. What type do I use for a function that calls both and just uses `?` to defer handling on errors from either function? Guess I either have to use Box<Error> and do a bunch of ugly downcasting whenever I want to handle an error or I have to write a bunch of code to define another error type E3 which combines E1 and E2 and I have to implement From<E1> and From<E2> so now I have more code just for defining error types and conversions than I have for actually doing anything.
3. Create complicated macros like error_chain[1] or failure[2] to try to cut down on boilerplate in #2.
Yes, that's the monadic equivalent to `catch(E1 e) {handle e}; catch(E2 e) {handle e}; catch(E3 e) {handle e}...`
On practice things get all over the place, with some times the exception being more concise and other times they being way more verbose. But the problem to be solved is not verbosity, it's with implicit code catching you by surprise, with too generic handlers on code that can't reasonably handle an error, and some issues with how errors interfere with code composition.
This is... not at all true? Rust already has the `?` operator, and there's no indication of this being replaced with do notation or anything like it.
Do you have any citation for this? That’s not my experience at all.
The part about people panicking everywhere is hyperbole (on the mood of the article). I've seen more panics than it makes sense and resorted to them too on disposable code, even they being more up-front work than handling it correctly.
That sounds more like laziness on the part of the programmer, rather than the language design.
Good languages account for this, and make the best choice the easiest choice whenever possible. If the best choice is twice as much work, there will be a lot times when the language encourages the programmer to do something else. I don't consider this a fault on the part of the programmer, I consider a fault on the part of the language.
I bet this generalization is wrong
No it doesn't. That complexity is there. What you mean to say is that it makes it more obvious (and far easier to debug) when things go wrong. Manual error handling ... let's just simplify that to "igoring all errors", and if you aren't convinced, looking at a few Go repositories on github will drive the point home.
So all "no exception system" really does is switch the default from "crash on error" to "ignore error". In C, before Java, this caused quite a few disasters with VERY bad error behavior (like silently deleting/overwriting all data, or starting a new data file, which you now get to merge manually with the one it ignored, for instance) ... in fact unix C tools are still (and, imho, justifiably) famous for their "nuke the whole world upon encountering a slight syntax issue" behaviors.
Besides, the Go authors' own tools usually crash (log.Fatalf), but without stacktrace or unwind, on 90% of errors. That's, of course, even worse than exceptions crashing out of the program in several ways:
1) You have no idea what happened. We're back to grepping the source code for the last string it printed and praying you find something. Stacktraces ! GIVE ME BACK MY STACKTRACES.
2) Cleanup does not happen (!!!!!) with log.Fatalf. I repeat: NO CLEANUP WILL HAPPEN WHEN CALLING log.Fatalf. DO NOT USE log.Fatalf if your program has done anything at all !!!!!!!!
The go authors themselves, especially should have gone through the exercise of, at EVERY error check, figure out what needs unwinding (file closing, ...) and then doing the same at higher levels of the stack as well. Needless to say, NOBODY, not even Rob Pike himself, has done that. As far as I can tell, not even once.
3) we all know how these bugs get fixed. By changing "crash out" to ignore. And frankly, given point 2, I will actually call it an improvement.
panic/recover is real exception support already. In principle, panic/recover and exception/catch are the same thing. Go and Rust support exception since the first days of their formal releases. It is just that they don't recommend to abuse the exception mechanisms.
Rather than catch littered everywhere, there is just a top level thing to do something like log a “well it died, cuz <<reason>>, thought you should know (moving on to the next thing)”.
Why not? They let programmers concentrate error-handling in specific points of execution while taking care of every single resource cleanup task the programmer sers fit to perform.
Is there any better way to tackle this problem and keep the source code readable?
1. Exceptions suck! Look at the mess in Java and C++. In our new language we're not going to have exceptions. We get rid of all that complicated "unwinding" stuff, too.
2. Look at all that error handling code in our new language! 2/3 of the lines are about errors. For the really bad ones, we'll have "panic", and you can't recover from a panic.
3. We need to recover from panics in production! So we'll add a catch or recover mechanism.
4. When we recover, everything is messed up and we get memory leaks and stuff that's not closed! We need proper unwinding so files and connections get closed.
5. OK, now we have all the machinery for exceptions, but don't use them! Exceptions are bad! Except where cool people use them. Besides, their syntax sucks and it's hard to send useful info with a panic.
6. Oh, hell, put real exception support in the language.
Go and Rust went through each of those steps and are now at step 5.