20 ms·
Where Everything Went Wrong: Error Handling and Error Messages in Rust (2020)
- jstrong 6y agoas someone who works 95% of the time in rust, how to handle my errors is never a problem for me. I think the issue is the expectations of people coming into it with a different frame of reference, expecting the kind of traceback/exception framework built in - and for that to be a typical debugging workflow. like 95 times out of 100, I just `.map_err(|e| /* code to convert e to return type's error... */)` and I'm done. the other 5 times I create a local error enum to represent the different types of error that can be returned. most importantly, the overwhelming gladness I have that code somewhere deep in my stack can't throw an exception vastly outweighs the "pain" of the ongoing incremental work it takes to handle errors intelligently throughout my code.
- rklaehn 6y agoRust error handling is great for reliable systems. But it used to be really annoying when just doing exploratory coding. With the anyhow crate, this problem is solved as well. Just use anyhow if you just want to try something out quickly without being slowed down by error type mismatches, and then later refine it to a handcrafted error type. I prefer rust error handling over all other languages I worked with (scala, java, C++, C, javascript, typescript, ...).
- egnehots 6y agofor exploratory coding you can also just unwrap, assert, panic.. It's useful to remember that error handling is optional in those cases :)
- unrealhoang 6y agoanyhow with ? is even shorter than unwrap. If it’s likely to have error at some point, I’d throw in a .context, it is so convenient. Edit: typo.
- edflsafoiewq 6y ago? requires the return type be annotated with an error type and the success case be annotated with Ok, for all functions up the stack, between the current function and where handling occurs. unwrap is purely local.
- rklaehn 6y agoI somehow still prefer ?, even in places where unwrap would be perfectly fine, like tests. unwrap is somehow unpleasant to look at. E.g. you can have tests that return an anyhow::Result<()>, and use ? anywhere in the test. The test will fail if the result is not Ok(()). Sure, you have the additional boilerplate of the return type annotation and the final Ok(()), but the test logic itself reads nicer, I think.
- simias 6y agoCompletely agree. I used to create my own error type(s) manually and implement the various conversions for 3rd party crates and it was quite a lot of boilerplate but with anyhow (for applications where you don't really care about strict error types) and thiserror (when you need to be a little more thorough). Actually that's effectively TFA's solution, if they had used thiserror/anyhow from the start there might not have been an issue to begin with. Admittedly it took me a while to find these crates since they're not part of std. Maybe the community should come together to create a curated list of third party crates that should probably be known by all Rust devs? Crates like thiserror, anyhow, rand, serde, clap and other "de-facto standard" crates?
- wronghorse 6y agoThis is really interesting because I recently found the anyhow crate and I was wondering why it's not mentioned anywhere else. Definitely feels like useful information that a newly minted Rust dev could make use of.
- simias 6y agoIndeed. I wish I learned about it earlier myself, it would've saved me some boilerplate in the past.
- nwellnhof 6y ago> Rust error handling is great for reliable systems. Last time I checked, Rust couldn't even catch malloc failures.
- wizzwizz4 6y agoTo be fair, these days malloc doesn't fail; your program crashes when it tries to use the memory, and there's nothing you can do about it. (But I agree that this is a problem with Rust's alloc system.)
- folmar 6y agoIt can, it just doesn't when the 0physical memory is exhausted, but there are other conditions $ ulimit -v 10240 < printf("%p\n", malloc(1024*1024*128)); > nil
- pedrocr 6y agoRust also has the traceback/exception error handling mode in the panic cases. And since there's no standard way to check those at compile time you're left to fuzz the code to find all the possibilities (or use some hacks). It would be great if the compiler could be put into a mode where panics are handled like errors that need to be explicitly handled. Maybe something like: 1) At the function level or crate level being able to specify if the code can or can't panic, sort of like safe/unsafe function signatures. 2) Being able to have blocks of code return MaybePaniced<T> and have to turn that into an error or other result. If it's not handled and the function/crate is marked as nopanic the compiler rejects the code. It would make the code more robust, and allow for an ecosystem where just like nostd you can specify that your crate is nopanic which helps in some environments.
- johnsoft 6y agoI found a crate that claims to do #1: https://github.com/dtolnay/no-panic https://github.com/dtolnay/no-panic This also looks interesting: https://github.com/Technolution/rustig https://github.com/Technolution/rustig
- pedrocr 6y agoYep, the no-panic crate is the hack I mentioned. It's using the linking process to fail compilation if I remember correctly. It only works on individual functions. rustig I didn't know and looks very interesting, thanks. Having it integrated in the compiler as annotations and guarantees would be ideal.
- safarimonkey 6y agoHere is one (recently created) that operates on the whole program rather than a single function: https://crates.io/crates/no-panics-whatsoever https://crates.io/crates/no-panics-whatsoever
- kstenerud 6y agoError handling has been wrong since the beginning, and has continued to be wrong ever since. First, we had error codes. Except these were wrong because people forget all the time to check them. Then we had exceptions, which solved the problem of people forgetting to check by crashing the app. Then the Java team got the bright idea to have checked exceptions, which at first helped to mitigate crashes from uncaught exception, but caused an explosion in thrown exception signatures, culminating in "catch Throwable". Back to square one. Then we got multi-return error objects, maybes, panics, and all sorts of bright ideas that fail to understand the basic premises of errors: Any error system that relies upon developer discipline will fail because errors will be missed. Any error system that handles all errors the same way will fail because there are some errors we can ignore, and some errors we must not ignore. And what's ignorable/retriable to one project is not ignorable/retriable to another. Attempting to get the complete set of error types that any given call may raise is a fool's errand because of the halting problem it eventually invokes. Forcing people to provide such a list results in the Java problem for the same reason. It's a hard problem, which is why no one has solved it yet.
- Ygg2 6y ago> It's a hard problem, which is why no one has solved it yet. The way you described it seems not so much as hard problem. More like an impossible one. That said. Perhaps error handling isn't one problem at all, but several problems masquerading as one. In which case Rust approach makes much more sense.
- mhh__ 6y agoThe only system I think should completely go is Exceptions except in the case of termination or absolutely catastrophic failure - this isn't really about programming but rather that the implementation is a total pain, the compiler struggles to optimize them, and even better they make quite a few safety analyses like borrow checking very difficult because the control flow graph basically explodes when you start considering exceptional control flow - even on a basic block
- simiones 6y ago
- EugeneOZ 6y agoError handling in Rust is the most appealing feature for me. Not performance. I really can not understand people who want to ignore error cases or write their code pretending that error will never happen. All of these "unwrap", "expect" sounds to me like "ok, there might be an error but I'm too lazy to write code to handle it so let's just hope it will never happen”. Of course, when your code is full of “panic”s, then errors handling will be a mess, but it just means your code is a mess and you need to finally take care of the errors. ‘Result’ and ‘enum’ are the best friends in this. By writing the code to handle errors you’ll find that many of them are recoverable; that some errors might look “critical” for one function and “insignificant” for another, so if you will not panic and just return this error, at some point of the path it will be recovered. This post mentions some useful libraries for simplifying the errors tracing (with contexts), but the tone is too sarcastic, sounds close to hysterical.
- Subsentient 6y agoFor the mostpart, Rust error handling is okay. What really rustles my jimmies, however, is the often mandatory indentation because of a lack of an inverse "if let". I prefer to bail out of a block if a condition is NOT met, rather than execute another nested block if it IS met. Rust makes that harder than it should be. It's good code hygiene in every other language, and Rust makes it painful in places. I've even been stopped from doing this by literal bugs in the borrow checker.
- conradludgate 6y agoYou can do something like that ``` let x = Some(1); let x = match x { Some(x) => x, None => return, }; assert_eq!(x, 1); ```
- Zababa 6y agoHN doesn't uses Markdown syntax for formatting: "Text after a blank line that is indented by two or more spaces is reproduced verbatim. (This is intended for code.)" https://news.ycombinator.com/formatdoc https://news.ycombinator.com/formatdoc
- seeekr 6y agoCouldn't this "inverse if let" be provided with a simple macro? Is that where you encountered bugs in the borrow checker?
- EugeneOZ 6y agoTry methods "is_err", "is_ok"; or for Option: "is_some", "is_none".
- duckerude 6y agoThat's not great if you still want to use the underlying value, because then you need to unwrap it later.
- tiddles 6y agoThe most common time this comes up for me is trying to use continue/break if a value is None/Err : loop { let thing = match foo() { Some(bar) => bar, None => continue, } } Yet I still always first try to put the continue in an unwrap_or_else first..
- jimis 6y agoI find this article hard to read. But I believe its point stands. In Rust: - You bubble up errors using `?` operator, - then you get a nice error message. - However the location of the error is lost, the more complex the program, the harder it is to figure out where "permission denied" for example comes from.
- nivenkos 6y agoYou can attach context to the errors though. It's only really a pain when writing libraries as you have to be so specific about your error types, etc. Overall, it's probably still the "least bad" option compared to other languages' approaches (both Go and Python are painful for this for example). But it can be a lot of extra work sometimes, especially when just working on the first basics of a library crate.
- magicalhippo 6y ago> You can attach context to the errors though. I'd say you should attach context. If there's problems accessing a file, then having an error message without the filename in it is near useless.
- jandrese 6y agoThe whole article was basically the author figuring out how to get a full path error message instead of only getting the first or last component of the error path. Also, how to do it without also crashing the program entirely. Plus of course getting some context about the state of the program when it crashed, and without imposing the full stack trace overhead since this error was supposed to be available even on production builds.
- Veserv 6y agoThe author appears to be trying to use error messages in their own code to debug it which is a pretty weird way to use error messages. Error messages are for humans and users. If you are developing your own code you can just use a debugger to debug the problem. Assuming they only had access to basic tools they could have just plopped down a breakpoint in the relevant error return from copy_from_process() and slowly walk up to discover their nullptr error pretty quickly without any code changes. If they had modern tools they could have just turned on tracing and then run backwards to figure that out and where the nullptr came from.
- iudqnolq 6y agoI've found the rust debugging experience to be very primative. When you say modern tools, are you describing rr? As far as I know that doesn't reliably integrate with rust?
- mathstuf 6y agoI've used gdb just fine on Rust code. Pretty printing isn't always as good, but if Rust is crashing, I'm probably down towards the syscall level anyways (or blowing the stack size as my last observed crashes were triggering).
- iudqnolq 6y agoI've had similar experiences. Inwas referring to the parent arguing that using Results and error enums to debug your own code is silly when you could just step backwards in a debugger. In my experience the rust debugging story isn't fluid enough that it's better than logging to debug most logic bugs.
- jiehong 6y agoIt could also just be a problem that appears in production, where there is no way to add breakpoints after the fact (even more so if this only occurs very rarely).
- jgilias 6y agoThe rule of thumb for me is `thiserror` for libraries, `anyhow` for executables. Seems to work well enough in the vast majority of cases. I do agree that the Rust way can be frustrating at first. But then, at some point it becomes clear that being forced to keep your error conditions in mind at all times is actually a healthy thing. Then going back to languages where code may fail anywhere seems less than optimal. Sort of like the difference between securing your ropes and just crossing fingers. As a side note, I've found that getting used to `Result` actually gives an added benefit of getting comfortable with the idea of such computational contexts in general. Be it Options, Maybes, Futures, Promises, Haskell IO, or anything of that kind.
- da-x 6y agoWhy not `thiserror` for executables as well? It happened to me a few times that I started to write an executable program, but then realized I want to embed its functionality in a library. Converting from `anyhow` to `thiserror` at that stage would be extra work that can be avoided.
- jgilias 6y agoI guess this depends somewhat on the situation. If the design is pretty clear upfront with a part that can be implemented as a core library, I'd make the library use `thiserror` from the beginning. However, if it's not really clear and I have to start with exploratory coding, then keeping track of error types that may come and go feels like unnecessary overhead, when I can just use `anyhow`. But! To each their own!
- status_quo69 6y agoI've used `thiserror` with great success in small side web projects since you can create a new enum of Errors that can be converted from a class of underling library errors. Then, inside of my regular application code, I sprinkle in `anyhow` to make my life easier. For example, if I wanted to say, return a 500 status code for all diesel database errors, I can convert the diesel error into my custom error type, then throw it back up the stack using `anyhow`. This works _really_ well in conjunction with Rocket's Responder impl. EDIT: This is pretty close to what TFA is saying as well, I should have read more in the article, heh
- nullc 6y agoSomeday I will attempt to use some rust software from the internet without almost immediately running into a panic with some inscrutable message. Maybe that day will be a day after _f$#$@@#$_ _crashing_ stops being the easiest and most idiomatic way of handling unexpected conditions.
- pferde 6y agoI prefer it when a program just gives up and crashes when it encounters an unexpected condition, instead of trying to soldier on and later possibly corrupting some data or state I care about, because it was working under incorrect assumptions. Now, the amount of unexpected conditions should be kept to a minimum, essentially just things outside of control of that program that the program cannot verify reliably. The rest should be _expected_ conditions, and error-handled properly.
- nullc 6y agoIn C applications people will generally fprintf(stderr, "Froboz option requires a grimulax!\n");exit(EXIT_FAILURE); Which is pretty useful. In rust, my experience is that it's far more common to unwrap an empty result and just toss a panic which spams some almost completely inscrutable error at the user. I'm not sure what the cause is-- it's been suggested to me by a heavy duty rust user that part of it is norms for library code (where exiting-as-error-handling would not be very welcome) and applied them to the whole ecosystem. I've also been told that it's not uncommon for people to use library code for handling stuff that would be manually written in C but then the library code under-documents what the failure cases are and so the caller can't easily/obviously do anything more than panic on an empty result. Whatever the cause the end result is that literally every single program I pull off github written in rust I encounter some panic. In fact, as of a about a month ago even firefox started regularly crashing for me w/ some panic in servo (triggered by twitter). The majority of the panics I get from stuff are just trivial "You used the wrong command-line options", but it's hard to resist the impression that software written in rust is simply a lot lower quality. It might well be that some of this is software that wouldn't even have been published had it been attempted in C, or that it's just because a greater percentage is written by developers who are new to programming (or at least new to systems programming)... but that isn't observable. The net result though is that it leaves me feeling uneasy and dubious about the claims of rust lowering defect rates, especially since there has been no formal validation of those beliefs. It sounds credible for sure, but if rust is freeing up programmers to worry about things other than memory safety, it's not showing from my experience. You can go back 5 years in my history and see that I promoted rust a lot -- I even opted for my former company to use it. But that was before encountering other people's rust code in practice. I think for years java suffered greatly from similar bad impressions-- where you don't expect to be able to run something you found without dealing with a maze of inscrutable null pointer exceptions.
- the_mitsuhiko 6y agoAt this point I'm quite happy with error handling in Rust. I just wish the backtrace member on the error trait was not nightly only and we had the ability to attach backtraces to errors.
- conradludgate 6y agoI'm pretty sure eyre[0] can provide backtraces in stable using `stable-eyre` [0]: https://docs.rs/eyre/0.6.5/eyre/ https://docs.rs/eyre/0.6.5/eyre/
- tiddles 6y agoI've migrated a few big projects through several iterations of different error handling crates, which was always painful. For now I've settled on thiserror, but only until backtraces are stable - then it's back to the churn :)
- protoman3000 6y agoCodepaths that can produce errors are just a monad. Separate this code from the rest and you have a solution for the problem. It’s Haskell’s do all over again and pure vs impure.
- the8472 6y ago> 'Dumping failed: "Failed in ptrace::read: Sys(EIO)"'. Or instead of trying to wrangle error messages, when a syscall falis you can strace or perf trace to find the offending stack, even in 3rd-party code, as long as it has debug info.
- scottlamb 6y agofailure [1] is still state-of-the-art, even though it's deprecated. It will get you backtraces (if RUST_BACKTRACE=1 is set), which nothing else will. I'm waiting until there are stable built-in backtraces [2] to switch to something else. I recently tweaked my application's failure reporting [3] to go from this embarrassing thing: E0211 073750.559 main moonfire_nvr] Sys(EROFS) to the more useful: E0211 111025.109 main moonfire_nvr] Exiting due to error: Failed to open dir /home/slamb/mymount/sample caused by: Unable to open meta file caused by: EROFS: Read-only file system (set environment variable RUST_BACKTRACE=1 to see backtraces) I should have thought long ago to advertise setting RUST_BACKTRACE=1 right in the error message, but better late than never. [1] https://crates.io/crates/failure https://crates.io/crates/failure [2] https://github.com/rust-lang/rust/pull/72981#issuecomment-726180149 https://github.com/rust-lang/rust/pull/72981#issuecomment-72... [3] https://github.com/scottlamb/moonfire-nvr/commit/9a5957d5efa54977924bd803c81166ac9f5e1eac https://github.com/scottlamb/moonfire-nvr/commit/9a5957d5efa...
- Animats 6y agoPython eventually reached a good system, with an official exception hierarchy. Originally, exceptions could be any type. By Python 2.7, new exceptions had to be derived from something already in the official tree. If you catch an exception in the tree, you also catch anything subclassed from it. You want a hierarchy where there's a subtree for external events, like network and file issues, and a subtree for internal program failures. That lets you catch external events and retry or something. Python 3.x has a different exception hierarchy, and it's worse. Too much is too close to the root, which leads people to catch "Exception". That catches internal program errors along with network errors, which is not helpful.