Starting a new Rust project right, with error-chain 2016-11-30

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.