When I began looking into best practices, I came across quite a bit of outdated advice to use the failure crate. These have made failure less needed in general and have sparked a number of more modern libraries taking advantage of these improvements to offer better ergonomics.
After reading through quite a lot of historical context and evaluating a number of libraries, I’ve now settled on a (largely library-agnostic) pattern for structuring errors, which I implement using the anyhow and this error crates. Introduce a relatively trivial word-counting application to explore and explain the problem space.
While the underlying error cause (“broken pipe”) is made clear, we’re missing a lot of context. Iterating over reader.lines() errors because we injected an implementation of std::Io::Read that fails on the first call to read().
Now imagine an error happening 5 calls deep inside a library within a much larger piece of software. Without any information on the chain of events in such a case, it quickly gets very difficult to understand what might be causing the error.
Earlier on I mentioned two different libraries, anyhow and this error (though both are by the same author, today). They may produce new errors internally, but these are unlikely to require special structure and can be more easily changed at will.
Applications may also want to parse and inspect errors, for example to forward them to exception tracking services or to retry operations when doing so is deemed to be safe. Custom result types like failure::Fail may not compose well with other parts of your user’s code and force them to learn yet another library.
Coming back to our word counting example, imagine we want to make count_words available as a public library. You wouldn’t normally do this for such a small and simple piece of code, but there can be value in making functionality available through public crates within larger projects.
As a demonstration, we can define boundaries in our word counter to separate this code into a library and an application part. I’ll highlight relevant parts below, but if you want to skip ahead, you can find the complete src/word counter.rs on GitHub.
While we could implement this by hand, this error allows us to avoid writing lots of boilerplate code: (Quoting the official documentation: “This error deliberately does not appear in your public API.
We iterate over the lines from reader, which get returned as Io::Result
If we didn’t care for the specialized WordCountError::Readers variant, this means we could have also written our code as follows, in which case we no longer need to use map_err() and can use ? With the API above in place, we can adjust the rest of our code to handle application-level concerns like argument parsing and invocation of word counter::count_words.
This provides valuable information to the user of the application about what was being attempted should an error occur. In both cases our error message now includes the name of the file we were working with.
So far we haven’t talked about backtracks, which are a common tool to use when debugging complex issues. At the moment, support for backtracks is only available on nightly Rust though, as the std::backtrack module is currently a nightly-only experimental API.
I generally find Rust ’s backtracks too cryptic and confusing to be of much help so their lack of support on the stable channel hasn’t been a problem for me personally. One thing is for sure though: The story for error handling has come a long way and with the current state of Rust, you can write very robust software in a pleasant and practical manner.
If you did, please consider sending a quick thank-you note, either through email or via a tweet to @NickGroenen. I think the bit about error handling being different depending on if you’re writing a library vs an application is simplification that’s common in the rust community but also a source of confusion.
The reasons for using anyhow vs this error aren’t really based on if it’s a library or an application, it’s actually about whether you need to handle errors or report them. Burnt sushi (from rip grep fame) agrees with a lot of my points but also challenges the use of proc-macro based libraries like this error for certain use cases, primarily due to the increase in compilation times that result from their use.
To add to his point, he shows us how to write the WordCountError implementation from this article by hand. In languages like Java, JS, Python etc, you usually throw exceptions and return successful values.
Let’s start with the simplest scenario where we just ignore the error. We’re prototyping our code and don’t want to spend time on error handling.
Note that unwrap is used in quite a lot of Rust examples to skip error handling. Let’s imagine that, for this program, that file is absolutely important without which it won’t work properly.
In some cases, you can handle the error by falling back to a default value. For example, let’s say we’re writing a server and the port it listens to can be configured using an environment variable.
If the environment variable is not set, accessing that value would result in an error. But we can easily handle that by falling back to a default value.
Here, we’ve used a variation of unwrap called unwrap_or which lets us supply default values. Using pattern matching to handle multiple or nested errors can make your code “noisy”.
Most errors generated by application code won’t be handled but instead logged or reported to the user. Errors from your library are often handled by your consumers, so they need to be structured and easy to perform exhaustive match on.
Let’s start by creating an ENIM to hold our two error variants: But this should be sufficient as a starting point for creating more complex and realistic custom errors.
Notice how unlike boxed errors, we can actually match on the variants inside MyCustomError ENIM. I hope this post was helpful in introducing the basics of error handling in Rust.