Handling errors in Rust


Error handling in Rust is pretty straightforward.

The standard library comes with the Result type which has the following definition:

#[must_use]
pub enum Result<T, E> {
    Ok(T),
    Err(E),
}

In short, Result can either be OK with a value T or be an error with value E. The #[must_use] annotation means that the compiler will warn you if you ignore a Result.

No more forgetting to catch that one exception or a if err != nil { return nil, err }.

Handling Result🔗

Let's have a look a small program to see the various way of creating and handling errors in Rust. Follow this playground link to run and play with the examples below.

use std::fmt;
use std::io;
use std::error::Error;

// Our set of errors for that program
#[derive(Debug)]
enum MyErrors {
    BadMood,
    // Badly implemented IO error
    FileFailure(String),
    // Correctly implemented IO error
    FileFailure2(io::Error),
}

// Impl display so we can have nice strings to print
impl fmt::Display for MyErrors {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            MyErrors::FileFailure(ref err) => write!(f, "File creation failed: {:?}", err),
            MyErrors::FileFailure2(ref err) => write!(f, "File creation failed: {:?}", err),
            MyErrors::BadMood => write!(f, "Nothing wrong, just wanted to error."),
        }
    }
}

impl Error for MyErrors {
    fn description(&self) -> &str {
        match *self {
            MyErrors::BadMood => "bad mood",
            MyErrors::FileFailure(ref err) => "file failure",
            MyErrors::FileFailure2(ref err) => "file failure",
        }
    }

    fn cause(&self) -> Option<&Error> {
        match *self {
            MyErrors::FileFailure(_) => None,
            // the cause is the io::error
            MyErrors::FileFailure2(ref err) => Some(err),
            MyErrors::BadMood => None,
        }
    }
}


impl From<io::Error> for MyErrors {
    fn from(err: io::Error) -> MyErrors {
        MyErrors::FileFailure2(err)
    }
}


fn will_succeed() -> Result<bool, MyErrors> {
    Ok(true)
}

fn will_fail(filename: &str) -> Result<(), MyErrors> {
    Err(MyErrors::FileFailure(format!("Failed to created {}", filename)))
}

fn do_something() -> Result<(), MyErrors> {
    match will_fail("index.html") {
        Ok(_) => {
            // we don't care about the result value, it's an empty tuple
            return Ok(());
        }
        Err(e) => {
            println!("Error while creating a file:\n {}", e);
        }
    };

    // Equivalent to match above
    if let Err(e) = will_fail("index.html") {
        println!("Error while creating a file:\n {}", e);
    } else {
        return Ok(());
    }
    // method 1 to propagate errors: try! macro
    let did_succeed = try!(will_succeed());
    // method 2 to propagate errors: question mark operator
    let did_succeed2 = will_succeed()?;

    Ok(())
}

fn main() {
    do_something();
}

About half of the lines are about defining errors, we will see how to reduce that boilerplate in the following section. If you want to read more on that, the error handling section in the Rust book is very good.

The interesting part in that code is the body of the do_something function which showcases the various ways of handling Result.

You can be in 2 situations when handling errors:

  • you want to handle them immediately
  • you want to do an early return and pass them back to the caller

The match and if let constructs are equivalent in this case and used if you are in the first situation.

The try! and question mark operator are also equivalent: they return the error if there is one and unpacks the Ok value otherwise.

? was stabilised in Rust 1.13 (released about one month before this post) and is somewhat controversial as some think that error handling is hidden when using it.

I like the ? operator myself since I think it makes the code neater but I let you be the judge:

let val = try!(try!(try!(do_something()).do_something_else()).finish());
// or cleaner
let a = try!(do_something());
let b = try!(a.do_something_else());
let val = try!(b.finish());

let val = do_something()?.do_something_else()?.finish();

Avoiding error boilerplate🔗

As you saw from the example above, defining your own errors is very verbose.

After experimenting on my own at first, I found the quick-error crate which makes creating your own error and extending built-in ones like the io::Error in the previous section a breeze. This was my go-to crate for error handling, until reading this article about error-chain.

error-chain builds on quick-error and makes it even more painless.

I have switched Tera 0.5 to use error-chain and am very happy about the end result.

The errors.rs file in Tera went from ~80 lines and lots of custom errors to:

error_chain! {
    errors {}
}

There is nothing you might say. And you would be somewhat correct! Using the error_chain! macro gives me Result, ResultExt (a trait), ErrorKind and Error but I didn't define any custom errors myself.

It's obviously not always empty though, here's the errors.rs of a static site engine using Tera:

use tera;


error_chain! {
    links {
        // Links Tera errors to that crate
        Tera(tera::Error, tera::ErrorKind);
    }

    foreign_links {
        // Link with errors not defined with error-chain
        Io(::std::io::Error);
    }

    errors {
        // I'm using this one lots of time so creating it there to keep it DRY
        InvalidConfig {
            description("invalid config")
            display("The config.toml is invalid or is using the wrong type for an argument")
        }
    }
}

The article linked previously made me realise that you need custom errors in 2 occasions: the user of the library will pattern match on them or you want to avoid repeating yourself like the InvalidConfig above.

In Tera case, I was able to replace all the errors with bail! macro that comes with error-chain. This is a very simple macro that works similarly to println! except it returns an error with the text given:

if something_is_wrong {
    bail!("Something wrong happened while doing {:?}", action);
}

// expands to
if something_is_wrong {
    return Err(format!("Something wrong happened while doing {:?}", action).into());
}

It doesn't look like much but using stringly typed errors saves a lot of time and makes you write better errors at the same time as you can write very specific errors without any boilerplate.

But the killer feature of error-chain is to chain errors, as its name implies. You often want to add context to errors and chaining allows just that.

The easiest example is a function to open a file: Rust doesn't include the filename in the error but you usually want it if you are going to display it.

use errors::ResultExt;

File::open(path)
    .chain_err(|| format!("Failed to open {}.", path))?
    .read_to_string(&mut content)?;

chain_err is coming from the ResultExt and is where the magic happens. If an error in File::open happens, it will create a new error, storing the one caused by File::open as its cause. Errors can be chained multiple times, allowing you to annotate errors at several levels and giving detailed error messages.

Printing all chained errors is as simple as the code below:

println!("Error: {}", e);
for e in e.iter().skip(1) {
    println!("Reason: {}", e);
}

I'm liking this approach quite a lot and will be using it for all my projects for the foreseeable future!