Starting a new Rust project right, with error-chain
While I was preparing the most recent release of error-chain, it dawned on me that not everyone knows the magic of error-chain, the little Rust library that makes it easy to handle errors correctly. Error handling is front-and-center in Rust, it’s subtly complex, and it’s important for the health of a project, and the sanity of its maintainers, that error handling is done right. Right from the start.
The first thing I do - really, the very first - when starting any project in Rust is deciding the error handling strategy. And with error-chain, I know up front exactly what I’m going to do, and that it’s going to work reasonably well. That gives me peace of mind.
So this post is to demonstrate what I consider the basic best practice for setting up error handling in Rust. If you follow these simple instructions right from the start you will have all your error scaffolding set up in a way that will scale as your project grows across many crates, no thinking required.
The error-chain quickstart.rs file demonstrates a simple, but powerful, application set up with error-chain. It’s suitable for using as a template - just literally copy that file to your main.rs and you will have an application set up for robust error handling.
I’ll reproduce it here in its entirety:
// Simple and robust error handling with error-chain!
// Use this as a template for new projects.
// `error_chain!` can recurse deeply
#![recursion_limit = "1024"]
// Import the macro. Don't forget to add `error-chain` in your
// `Cargo.toml`!
#[macro_use]
extern crate error_chain;
// We'll put our errors in an `errors` module, and other modules in
// this crate will `use errors::*;` to get access to everything
// `error_chain!` creates.
mod errors {
// Create the Error, ErrorKind, ResultExt, and Result types
error_chain! { }
}
use errors::*;
fn main() {
if let Err(ref e) = run() {
println!("error: {}", e);
for e in e.iter().skip(1) {
println!("caused by: {}", e);
}
// The backtrace is not always generated. Try to run this example
// with `RUST_BACKTRACE=1`.
if let Some(backtrace) = e.backtrace() {
println!("backtrace: {:?}", backtrace);
}
::std::process::exit(1);
}
}
// Most functions will return the `Result` type, imported from the
// `errors` module. It is a typedef of the standard `Result` type
// for which the error type is always our own `Error`.
fn run() -> Result<()> {
use std::fs::File;
// This operation will fail
File::open("contacts")
.chain_err(|| "unable to open contacts file")?;
Ok(())
}
It’s fairly self-explanatory, but I do want to point out a few things about it. First, that main function:
fn main() {
if let Err(ref e) = run() {
println!("error: {}", e);
for e in e.iter().skip(1) {
println!("caused by: {}", e);
}
// The backtrace is not always generated. Try to run this example
// with `RUST_BACKTRACE=1`.
if let Some(backtrace) = e.backtrace() {
println!("backtrace: {:?}", backtrace);
}
::std::process::exit(1);
}
}
This is typical of the main functions I write lately. The whole purpose is to
immediately delegate to a function that participates in error handling (returns
our custom Result
and Error
types), and then to handle those errors. This
error handling routine demonstrates the three pieces of information that
error-chain delivers from an error: the proximate error, here the e
binding;
the causal chain of errors that led to that error; and the backtrace of the
original error. Depending on your use case you may not bother with the
backtraces, or you may add in a call to catch_unwind
to deal with panicks.
If you run this example you’ll see the following output:
error: unable to open contacts file
caused by: The system cannot find the file specified. (os error 2)
This demonstrates the raison d’être for error-chain: capturing and reporting the
chain of multiple errors that led to the program failing. The reason we see both
the final error and its cause is because in our run
function we chained two
errors together:
fn run() -> Result<()> {
use std::fs::File;
// This operation will fail
File::open("contacts")
.chain_err(|| "unable to open contacts file")?;
Ok(())
}
That call to chain_err
produced a new error with our application-specific
error message, while storing the original error generated by File::open
in the
error chain. This is a simple example, but you can imagine in larger
applications the error chain can get quite detailed.
That’s how you get started with error-chain, but that’s not everything it does. For more read the docs.
Effective error-chaining
Just a few quick notes about how I use error-chain.
When I’m hacking I don’t hesitate to just use strings as errors, which can be
generated easily ala bail!("I don't like this: {}", nonsense)
. These are
represented as ErrorKind::Msg
, a variant defined for all error types generated
by the error_chain!
macro.
For applications, strings are often perfectly fine as the error type. When you
are designing an API for public consumption though, that’s when defining your
error kinds (using the error_chain!
errors { }
block) becomes important.
Having typed error variants gives consumers of your library something to match
on. error-chain gives you the option of doing the easy thing or the hard thing,
it scales with the needs of your code.
Do put your error_chain!
invocation inside an errors
module and import the
entire contents with use errors::*
. Glob imports aren’t something you want to
do a lot, but in this case the pattern is worth it: you really want these four
types to be at hand in every module of a crate.
I try not to rely too heavily on the automatic conversions from foreign_links {
}
. Foreign links are automatically converted to the local error type. They are
easy to set up and make errors outside your control easy to interoperate with,
but by taking the automatic conversion you lose the opportunity to return an
error more relevant to your application. That is, instead of returning an error
of “the system cannot find the file specified”, I want to return an error
“failed to open contacts file” that is caused by “the system cannot find the
file specified”. Every link in the error clarifies what went wrong. So instead
of using ?
on a foreign error, use chain_err
to give more context.
error-chain really shines once you start building up a constellation of crates
all using the error-chain strategy, all linked together via the error_chain!
links { }
blocks. Linked error-chain errors are able to propagate backtraces
and have a structural shape that is easy to deeply match, so that e.g.
your error that originated in your utils
crate, bubbled through your net
crate, then up through your app
crate is easy to pinpoint through pattern
matching like so:
// Imagine these are crates, not mods
mod utils {
error_chain! {
errors { Parse }
}
}
mod net {
error_chain! {
links {
Utils(::utils::Error, ::utils::ErrorKind);
}
}
}
mod app {
error_chain! {
links {
Net(::net::Error, ::net::ErrorKind);
}
}
pub fn run() -> Result<()> {
match do_something() {
Err(ErrorKind::Net(::net::ErrorKind::Utils(::utils::ErrorKind::Parse), _)) => {
...
}
_ => { ... }
}
}
}
This kind of deep error dispatching doesn’t happen often, but when it does, it’s nice that error-chain makes it easy. Again, error-chain lets you start simple, but gives you power as your project grows.
TL;DR
When you start writing a new Rust application, one of the first things you should ask is “how am I going to handle errors?”; and the answer should probably be “I’m just going to set up error-chain”. Configure error-chain using the quickstart.rs example, and don’t worry about a thing.
For more information about error-chain read the docs.