10 ms·
For beginners who are struggling with Rust's error handling, I wrote a post that goes through common use cases such as: * Ignoring the error * Terminating the
by rkwz 6y ago
For beginners who are struggling with Rust's error handling, I wrote a post that goes through common use cases such as:
* 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
http://www.sheshbabu.com/posts/rust-error-handling/ http://www.sheshbabu.com/posts/rust-error-handling/
- amelius 6y agoI'm curious, what did Rust developers think was wrong with exceptions? I'm pretty happy with them in other languages, as they allow me to focus more on the "normal" path in my code.
- steveklabnik 6y agoThe real core of it is that exceptions demand runtime support, and as a systems language, Rust runs in places where that may be prohibitive. Beyond that, there are other reasons, but in my mind, that is the fundamental issue. Everything else kind of rests on top of that.
- throwaway894345 6y agoOut of curiosity, how does C++ get away with it?
- steveklabnik 6y agoAt its core, compilers offer a flag that lets you turn them off, same as Rust. (-fno-exceptions is the moral equivalent of -C panic=abort) Though I believe that's technically a language extension, but IANAC++SL (I Am Not a C++ Standards Lawyer) 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.
- weinzierl 6y ago> At its core, compilers offer a flag that lets you turn them off, same as Rust. 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.
- saurik 6y agoHaving to throw ?s and unwraps on what amounts to essentially every single function call--as if you think your function can't fail you probably didn't think through what it does very well--is kind of ridiculous, though: good systems avoid error "handling" (so you never look at errors) and centralize error management and recovery (so you want error routing, which exceptions can do). Even Erlang and Haskell (yes, in addition to monads) have exceptions, and languages with true "monadic" error handling (which some claim Rust has, but it doesn't) manage to hide it all behind their monad application syntax. Rust has taken a "worst of all worlds" approach here, mixing all of poor performance, poor ergonomics, and poor functionality together to get their current error handling solution :(.
- dralley 6y agoThat is very much an opinion. It's very useful to know exactly which lines might be throwing an error. And I don't agree that "essentially every single function call" needs a ? or an unwrap(), that has not been my experience at all.
- deleted 6y ago[deleted]
- amelius 6y agoGood point, but C++ exceptions are expensive because they allow you to jump back multiple stack frames at once. If in Rust you're going to check every return value, then isn't that more expensive on average?
- steveklabnik 6y agoI don't have a great handle on the exact details, but my suspicion is that this kind of analysis is hard to do, because it can play out differently in the micro vs macro. That is, in a naive implementation, yes, but after the compiler is done inlining and DCE-ing and yadda yadda yadda things end up looking differently in real codebases than one might assume. 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.
- Ericson2314 6y agoThere are more branches, but the branch predictor should be able to figure it out. 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 https://www.ccs.neu.edu/home/shivers/papers/mrlc-jfp.pdf ).
- Rusky 6y agoThat would be neat, but it's not a pure win. It's more just a different set of trade-offs, because it defeats the CPU's return address stack predictor. 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.
- saurik 6y agoYou are also just wasting fast storage--often a register--tracking and checking this stuff (at least for any type for which there isn't a natural invalid value, such as a null pointer); multiple return points is going to do the same thing, but worse, as you will need to hand the second return point to the called function but will no longer be able to reuse invalid return values.
- saurik 6y agoExceptions are merely a syntax for a monad that can be trivially implemented as simply automatic syntax sugar for the bloated error result code stuff Rust is not only forcing at runtime (having every single function call return a successful error code result is _not free_) but requiring the user to explicitly type out while coding; if you are willing to swallow the former (which to me is silly, but Rust is already doing this) you can at least handle the latter, and you don't need "runtime support" to make it happen: it is a code generation issue and nothing more.
- steveklabnik 6y agoThat code you generate is what “runtime support” means, yes.
- gameswithgo 6y agoexceptions are in many ways equivalent to the result type. the explicitness of the result type can be nice, and you have more control over the performance implications. Shortcuts like the ? operator make it nearly as simple to work with most of the time.
- marcosdumay 6y agoChecked exceptions are a hassle. They don't mix well with other types, they don't generalize well. 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?
- throwaway8941 6y agoThat's not what safety means. panic! is perfectly safe Rust. https://doc.rust-lang.org/nomicon/meet-safe-and-unsafe.html https://doc.rust-lang.org/nomicon/meet-safe-and-unsafe.html
- dllthomas 6y ago> They don't mix well with other types, they don't generalize well. 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=24454663 https://news.ycombinator.com/item?id=24448865 https://news.ycombinator.com/item?id=24448865
- devit 6y agoImplicit exception propagation (like C++, Java, JavaScript, etc. do) is a very bad design because when a statement can throw an exception, you need to design your code to rollback any changes before it, so it's essential that you know at a glance which statements can throw exceptions (in Rust, the ones with a "?"). 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).
- golergka 6y ago> Implicit exception propagation (like Java I haven't touched Java in 10 years, but didn't it have explicit propagation with `throws` statement for this exact purpose?
- bluejekyll 6y agoThere’s always the feared NumberFormatException which is unchecked, for some crazy reason. 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.
- masklinn 6y agoJava has both checked exceptions (explicit `throws`) and unchecked (no explicit notification), and its checked exceptions are often avoided because the delineation is not very good and they're cumbersome to work with.
- devit 6y agoI mean the fact that you in Java, etc. any function you call may throw an exception, which automatically propagates upwards from the code that calls the function, which results in part of the code in your function being executed and part not being executed, without any obvious signs of that (like having an early return or a "?" in the function). 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.
- ovis 6y agoIt's interesting that I could write your last sentence, but replacing "exceptions" with "monadic error handling." 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).
- saurik 6y agoRust doesn't have "monadic error handling" as it doesn't have monads. Haskell has monads and syntax for managing them, so when you declare a type like Result (which is not a monad as merely a data structure) you hide it as a monad (wherein you implement the Monad trait and define how the Result type propagates errors) and then it just becomes part of the usage of do notation in the language (like any other monad). If Rust has gone with monadic error handling I would have been elated, but it didn't: it went for manual, but required, error checking, which was a bit ridiculous in the 2010s and the opposite of monads (which I would argue exceptions are much closer to: they are effectively a hardcoded implementation of Monad for an implicit type wrapper of every function result and a do notation you can't opt out of).
- golergka 6y agoRuntime overhead was already mentioned, but it's not the only reason. 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.
- setr 6y ago1. non-local control flow can make it difficult to read 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.
- maest 6y ago#12. is pretty convincing.
- jokethrowaway 6y agoThe problem for me is looking at code and not knowing whether it can crash my process or not. 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.
- MaulingMonkey 6y ago> I'm curious, what did Rust developers think was wrong with exceptions? 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.catch_unwind.html https://doc.rust-lang.org/std/panic/fn.resume_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.
- armSixtyFour 6y agoGreat post!