How to do error handling in Rust and what are the common pitfalls?

Rust generally solves errors in two ways:

  • Unrecoverable errors. Once you panic!, that's it. Your program or thread aborts because it encounters something it can't solve and its invariants have been violated. E.g. if you find invalid sequences in what should be a UTF-8 string.

  • Recoverable errors. Also called failures in some documentation. Instead of panicking, you emit a Option<T> or Result<T, E>. In these cases, you have a choice between a valid value Some(T)/Ok(T) respectively or an invalid value None/Error(E). Generally None serves as a null replacement, showing that the value is missing.


Now comes the hard part. Application.

Unwrap

Sometimes dealing with an Option is a pain in the neck, and you are almost guaranteed to get a value and not an error.

In those cases it's perfectly fine to use unwrap. unwrap turns Some(e) and Ok(e) into e, otherwise it panics. Unwrap is a tool to turn your recoverable errors into unrecoverable.

if x.is_some() {
    y = x.unwrap(); // perfectly safe, you just checked x is Some
}

Inside the if-block it's perfectly fine to unwrap since it should never panic because we've already checked that it is Some with x.is_some().

If you're writing a library, using unwrap is discouraged because when it panics the user cannot handle the error. Additionally, a future update may change the invariant. Imagine if the example above had if x.is_some() || always_return_true(). The invariant would changed, and unwrap could panic.

? operator / try! macro

What's the ? operator or the try! macro? A short explanation is that it either returns the value inside an Ok() or prematurely returns error.

Here is a simplified definition of what the operator or macro expand to:

macro_rules! try {
    ($e:expr) => (match $e {
        Ok(val) => val,
        Err(err) => return Err(err),
    });
}

If you use it like this:

let x = File::create("my_file.txt")?;
let x = try!(File::create("my_file.txt"));

It will convert it into this:

let x = match File::create("my_file.txt") {
    Ok(val)  => val,
    Err(err) => return Err(err),
};

The downside is that your functions now return Result.

Combinators

Option and Result have some convenience methods that allow chaining and dealing with errors in an understandable manner. Methods like and, and_then, or, or_else, ok_or, map_err, etc.

For example, you could have a default value in case your value is botched.

let x: Option<i32> = None;
let guaranteed_value = x.or(Some(3)); //it's Some(3)

Or if you want to turn your Option into a Result.

let x = Some("foo");
assert_eq!(x.ok_or("No value found"), Ok("foo"));

let x: Option<&str> = None;
assert_eq!(x.ok_or("No value found"), Err("No value found"));

This is just a brief skim of things you can do. For more explanation, check out:

  • http://blog.burntsushi.net/rust-error-handling/
  • https://doc.rust-lang.org/book/ch09-00-error-handling.html
  • http://lucumr.pocoo.org/2014/10/16/on-error-handling/

If you need to terminate some independent execution unit (a web request, a video frame processing, a GUI event, a source file to compile) but not all your application in completeness, there is a function std::panic::catch_unwind that invokes a closure, capturing the cause of an unwinding panic if one occurs.

let result = panic::catch_unwind(|| {
    panic!("oh no!");
});
assert!(result.is_err());

I would not grant this closure write access to any variables that could outlive it, or any other otherwise global state.

The documentation also says the function also may not be able to catch some kinds of panic.