* Ignoring the error
* Terminating the program
* Using a fallback value
* Bubble up the error
* Bubble up multiple errors
* Match boxed errors
* Libraries vs Applications
* Create custom errors
* Bubble up custom errors
* Match custom errors
* Ignoring the error
* Terminating the program
* Using a fallback value
* Bubble up the error
* Bubble up multiple errors
* Match boxed errors
* Libraries vs Applications
* Create custom errors
* Bubble up custom errors
* Match custom errors
As for implementation using unwinding vs a sum type, they are semantically equivalent, but using a sum type like Rust does allows you to use exceptions even when the error case is common without the steep performance degradation that happens with unwinding, at the expense of a very slight slowdown in the no-errors case.
Rust also supports unwinding/aborting exceptions as "panics". These are implicit because all accessible mutable memory is destroyed by a panic, since you must not keep references across catch_panics, so there is no need to rollback (except for unsafe code, in which case you need to assume that all function calls can panic).
I haven't touched Java in 10 years, but didn't it have explicit propagation with `throws` statement for this exact purpose?
If the exception is not a RuntimeException and not declared in the throws clause, then the compiler will error out, but in other cases the code will compile.
In Rust, you need to use the "?" operator when you call an exception-throwing function to propagate the exception, and if you don't you will get a Result, and a warning if you don't use it (i.e. not using "?" in Rust is like using Try in Scala).
If Rust code has no "?" or "return", then no exceptions will be thrown and the code will fully execute, except for aborts and panics which are guaranteed to make all visible memory inaccessible, so you will never see a partial modification of an object due to an unexpected exception being thrown.
Similarly, all unchecked errors and exceptions mean that you never know. So you have to treat critical sections of code as areas where resources need to be cleaned up by using try and finally blocks.
Citation needed on this one. In almost two decades I never once came across code that straight away required such rollbacks.
Consider a simple case where I am trying to receive data over a socket and write it to a file. I need to 1) open the socket, and 2) open the file. Whichever order I do these in, if the second one fails I need to be sure I clean up the former.
Use a protocol that can cope with a disconnect well. Write to a temporary file which either you then rename, or the OS discards if there was a failure. Trying to “clean up” manually can break things further since you don’t and can’t account for all possible reasons for a failure.
A simple example: your hard drive is failing, and performing any kinds of cleanups on it will get your program stuck even further. Also, does it really matter too much for your stated purpose if you get an ENOSPC, an EPERM, or an EIO?
Sure, in a sufficiently crazy situation, close() might not actually close the file descriptor - maybe I've somehow trampled on the memory associated with the function (having worked around any kind of W^X), or maybe the operating system is just seriously fucked - but it seems crazy to expect that on the basis of failing to open a file. You should still close the socket.
That cleanup phase is generally some kind of rollback, usually in the form of discarding whatever partially constructed datastructures (sometimes in the form of a savepoint and reverting, but thats rarer ime).
The other type of catch handling is to just force everything into some sane default state, to avoid having to care what specifically borked
Beyond that, there are other reasons, but in my mind, that is the fundamental issue. Everything else kind of rests on top of that.
I do believe there's been some discussions in C++ land about the relative performance of these approaches, though I can't find good references right this second.
Someday we should push multiple return points, one for each enum variant of the return type, and then we avoid the branches too. (This approach is described in https://www.ccs.neu.edu/home/shivers/papers/mrlc-jfp.pdf ).
For exception-like Result enums where you want to predict the Ok variant every time that's probably fine (as long as your CPU is new enough that taking the error path won't cause it to mispredict every return afterwards), but it may or may not be a win in the general case of enum returns.
The language also offers some more language support for declaring that some things will never use exceptions, via noexcept. This matters a bit more because, since the language itself has exceptions as a feature, some things assume that they exist in order to work properly, in my understanding.
That is true and in embedded projects it is common that exceptions are forbidden, which shocks many young developers who are not used to write C++ without exceptions;-) In C++ it's nice when you have exceptions but without them you are very much on you're own, almost as you were writing C. Additionally every library does error handling slightly differently and you quickly end up in a world of pain.
The really nice thing about Rust, in that regard, is that there is no need for different error handling systems. No matter if you are writing very low level code (e.g. embedded systems) or high level (e.g. web apps) code the basic error handling primitives are the same. Since we have the question mark operator the ergonomics are not too different from exceptions in other languages. What is not so clear at the moment is, what the best practices are on how to combine these primitives. As far as I understand this is one of the major areas the Error Handling Group is to address.
Implicit (unlike Java's throws declarations) exceptions fit well into a particular error-handling strategy of handling some expected errors, but by default, crashing the whole app on any unexpected error. It works pretty well for some applications, especially for web apps that operate on request-response model and have no local state: nothing could be easier than simply crash the VM, send the error report and restart the app back again.
However, this strategy doesn't fit all possible situations. Example, you may need to know when something can throw so you would be able to roll back to a valid state.
Often, what you want from a call to some third-party code is to explicitly define all possible error conditions that it can return. Java's `throws` handles that in some regard, but `Option` or 'Maybe` or `Result` monad (which has a lot of different names in different languages) is easier to reason about, and since it is usually implemented in languages that have pattern matching syntax, it also looks read to easier, more readable code, at least in my opinion.
Now, I have to admit that ? and ! syntax sugar in Rust look very exception-like, and when I wrote web apps with it, I found myself having absolutely the same mindset as I had with exceptions. With one important difference: if I would be so inclined, I could have very easily gone through all the codebase and made all error handling much more strict and explicit. Regardless of how giant the codebase would become, the type checker would have my back in this process, and if I didn't want to leave some unknown error condition left behind in the depths of the call stack, it wouldn't be.
2. it also makes it difficult to handle nested errors (eg the catch() itself throws)
3. unchecked exceptions (and implicit propagation) means any function can throw something, at any time -- it's not defined in the API contract -- so if you really wanted to be safe and general, every function needs have exception checking. This is the same problem that null has
4. It also means it can change "under your feet" -- a library can add new exceptions to functions with no indication; this is also possible in rust, but its requires much more finagling by the author.
5. Checked exceptions like in Java are basically the same as rust's error-as-enum, but feature the other points.
6. At least in Java, not all exceptions are checked, but of course that doesn't need to be true.
7. Through developer ergonomics, Exceptions leads the natural strategy of try it, and rollback on failure. Rust Result<> leads the natural strategy of validate, then continue. The latter, IMO, is significantly easier/general/more sane.
8. Through developer ergonomics (avoiding a try-catch every statement), multiple statements are naturally rolled into a single try-catch -- undone when this turns out not to be the case. In the worst case, if you try-catch every statement in a function, its completely unreadable. It's also difficult to know which function throws which error, if you have them rolled up in a single try-catch, with multiple catches.
9. Exception's main job is to put errors in the background; Rust has the philosophy of putting errors in the foreground
10. The main benefit they have over return codes is that return codes are utterly arbitrary, and even less defined in the API contract, or the language itself, than exceptions. They lose out on the other points -- namely that they're significantly more complex. Result<> grants the natural ergonomics of return codes, with a higher level of definition than exceptions.
11. Try-And-Rollback is a generally valid strategy for single-threaded code; less so for parallel code (eg fork-and-join). Result<> fits that model more cleanly.
12. My personally most important point: exceptions are ugly and anyone who likes them is ugly too
That's whatever I could think of off the top of my head.
I hate exceptions with a passion, especially if you throw in async exceptions.
All the new 500s we get at work are exceptions that we're not catching somehow because the developer who wrote the code didn't expect that scenario.
Unchecked exceptions are well, unchecked. Allowing unchecked things would completely compromise the Rust safety for no gain at all.
But well, I'm not sure what you are complaining about. The examples where the error is not treated at the point it happens look quite similar to what a try-catch would look. What code do you think error handling gets in the way of the "normal" path?
I think this is very true of Java checked exceptions. I am not sure this holds up if we actually offered a more sophisticated language for talking about exceptions.
Some of my recent comments on the topic: https://news.ycombinator.com/item?id=24454663 https://news.ycombinator.com/item?id=24448865
Ultimately, both are equally expressive and it's reasonable to personally prefer either. The "Result" type approach maps better to the concept of a program as a series of inputs and outputs (functions).
To be fair, Rust effectively does have exceptions, in the form of panics. They unwind the stack, can have different types (although &str/String are the most common/supported), can be caught and rethrown if not configured to abort the entire process instead.
They are used for exceptional, fatalish errors (including admittedly less exceptional bugs from unconsidered edge cases), and not regular "normal" / "expected" day-to-day error handling.
https://doc.rust-lang.org/std/panic/fn.catch_unwind.html
https://doc.rust-lang.org/std/panic/fn.resume_unwind.html
> I'm pretty happy with them in other languages, as they allow me to focus more on the "normal" path in my code.
This is the boon and curse of exceptions.
Consistently writing exception-safe code when you're focused more on the normal path can actually be quite hard, as evidenced by all the code I've debugged written by coworkers who've failed to do so. Reviewing said code to attempt to enforce exception safety is similarly hard. Rust's `?`s, by contrast, make early-outs relatively easy to spot and reason about via their syntax.
Aside from leaked memory, double frees, and other bugs and crashes caused by invalid object states from failing to consider the case where execution is interrupted mid-operation by an exception, there's also all the undefined behavior from exceptions unwinding across C ABI boundaries for callbacks and other purpouses. This is suprisingly common in gamedev.
The larger and more widely used the codebase, the more frequent and normal the "abnormal" path becomes, and the more serious the consequences of ignoring it. Nobody cares if a quick utility script or command line tool occasionally crashes if you can just rerun it, or have it quickly patched, and don't suffer data loss. Focusing on the normal path is probably the right choice there!
But at the other extreme, if you're about to release a game, a crash affecting 1% of players is still pissing off thousands of people, and not all handhelds have internet connectivity for post-release patching. This results in weeks-long QA processes to try and catch all the nasty stuff, because delaying the release and throwing a wrench into a million dollar marketing plan might be less expensive than shipping shit broken by something as mundane as an unexpected exception. Focusing on all the edge cases is often the right choice there.
And when focusing on all the edge cases, explicit return based error handling tends to be much easier to read and reason about than exception based code, IME - even in Rust where a lot of heavy lifting might be hidden behind the occasional stray "?" - letting me get back to focusing on the "normal" path of my next task quicker - and with more confidence that I considered the appropriate edge cases of the previous task exhaustively and successfully.