I've been wanting to write a post about error handling for quite some time now. Any non-trivial, robust software is invariably going to include a considerable amount of code to handle cases wherein errors occur during the program's execution. While computers themselves are wonderfully deterministic machines, marvels of engineering in their own right, computer and software systems, on the other hand, are often not quite as predictable. Robust programs have to deal with all kinds of sources of errors, which could be caused by anything from physical hardware problems to network communication problems between incompatible software versions to malformed data, etc. The fantastic Rust programming language gives us some very effective tools to the table to help programmers deal with errors in their programs. In this post, we'll walk through the most common methods of addressing errors in Rust programs and evaluate each one's elegance and effectiveness. The evaluation scores that I provide will, of course, be subjective to my personal taste and experience.

The Error Trait and Result Type

Before we talk about error handling, however, we need to make sure we understand how Rust represents errors and how functions can indicate that they encountered an error. The Rust standard library defines a trait, std::error::Error, that can be implemented by any type to represent any kind of failure. Any type that implements this trait provides two methods. The first, description, can be invoked to get a textual explanation of why the error occurred. When you implement an Error yourself, you'll want to use this method to explain to either your program or library's users what caused the error so that, hopefully, they can fix it without having to ask for more information from you personally. Second, the cause method can be implemented to indicate that another Error was the source of the error you are returning. This is particularly useful when implementing a library of your own, whose functions may have to call out to other libraries whose functions may produce an Error of another specific type.

Other languages have different ways of indicating to a function's caller that an error occurred, such as having functions return two values rather than just one, however Rust uses the std::result::Result type. Result<T, E> is a generic enum containing two possible variants. The first, Ok, can contain a value of type T and, when returned by a function, indicates that no error occurred and a valid return value has been produced. The second, Err, can contain a value of type E, which can actually be any type, but is almost always a type that implements the Error trait. Functions written in Rust that may produce an error will typically return a Result rather than try to address errors for you, given that they don't know the context they may be used in. Thus for example, a function that makes an HTTP request to an API to download some JSON data and parse it into a struct might have a signature like the following.

fn get_location_info(address: &str) -> Result<Location, APIError>

A useful convention in Rust to be aware of is that, in the case that a function does not have a meaningful return value but may still encounter an error, a return type of Result<(), E> may be used. That is, the Ok variant of the return value will contain unit: ().

Unwrapping Errors Using "match"

Likely the first method for handling errors that early Rust programmers will encounter involves the use of Rust's powerful match expression. In part, the match expression lets us do pattern matching to destructure the possible values that a binding (or variable, if you prefer) contains and then return some value according to each case. match can thus be used quite simply to destructure a Result's Ok and Err variants and extract either one's contained value, like so.

let location_result = get_location_info("Tokyo Tower");
let address = match location_result {
    Ok(address)  => address,
    Err(api_err) => return Err(api_err)
};
println!("Tokyo tower is located at {}", address.coordinates);

This approach to handling errors is extremely effective, in that it allows us to very explicitly handle the possible success and failure cases involved in invoking a function. However, this approach is not particularly elegant for precisely the reason that it is so effective. By explicitly unwrapping the possible Result variants, we are forced to deal with each error right away. A lot of the time, we want to write functions that will simply return the first error it encounters, to leave it to the caller to decide what to do with the error, rather than trying to handle events that we might not be able to anticipate, and using match would force us to write a return clause every time we unwrap a Result. This approach should be used to terminate the propagation of errors in the place in your code where you can be the most sure that you can take a suitable action for any given error, and do not need to compose the result of that function's return value with others invoking it.

Effectiveness: 10 / 10

Elegance: 2 / 10

Unwrapping Errors Using "if let"

Similar to using match, the if let syntax supported by Rust allows us to use pattern matching to attempt to destructure some value and then execute a block of code if the pattern matched successfully.

let location_result = get_location_info("CN Tower");
if let Ok(address) = location_result {
    println!("The CN Tower is located at {}", address.coordinates);
}

This approach is perhaps best used when we only want to take some specific action in one particular case. It is possible to chain multiple if let expressions using if let / else if let, however as soon as you have more than one case to handle, you're much better off using match. This method is almost as effective as match, being slightly less so because it is best only used to handle one case, however it is slightly more elegant. Using if let, we can somewhat more elegantly handle one case, which we may want to do if we either don't care about any other cases or want to perform some action with side effects, but match is usually a little better.

Effectiveness: 7 / 10

Elegance: 6 / 10

Method Chaining

The designers of Rust's standard library realized that using match to unwrap every Result encountered in order to apply a transformation to contained values would be a bit cumbersome. So, to make it a little easier to "do stuff" with whatever value a Result may contain, they wrote methods like Result.map, Result.map_err, and Result.and_then to make it easier to apply transformations to a Result's contained value. Referring back to our example of invoking a REST API endpoint, we might implement our get_location_info function (naively) like so.

fn get_location_info(address: &str) -> Result<Location, APIError> {
    let client = Client::new();
    let endpoint = format!("https://my.location.api/address={}", address);
    client.get(&endpoint)
        .send()
        .map_err(From::from)
        .and_then(|mut response| {
            let mut body = String::new();
            response.read_to_string(&mut body)
                .map_err(From::from)
                .map(|_| body)
        })
        .and_then(|json_data| json::decode<Address>(&json_data))
}

Here, we are supposing that our imaginary APIError implements the std::convert::From trait in such a way that we can convert from either an HTTP request error, a response reading error, or a JSON decode error into APIError.

Hopefully it is relatively clear that what this code is doing is, after setting up an HTTP client and the endpoint URL string, it sends an HTTP GET request and then attempts to read the response's body before parsing it, treating it as JSON, into an Address struct. Because we are method chaining on the Result type, we never actually stop working with a Result. That means, at the end of the sequence of method invocation, we can simply return the final Result instance, after having applied any appropriate closures (supplied to and_then, map, and map_err).

This approach felt, to me, anyway, like the best way to do things after having written a lot of backend Javascript using Promises. The approach is sufficiently effective to handle all of the variants a Result can take, and even lets us fairly elegantly (i.e. without unwrapping) chain operations on each variant using Rust's closures. However, with regards to elegance, code written in this style can quickly become quite unappealing and hard to deal with as we indent several times in order to apply operations to intermediate results (such as that of response.read_to_string) in order to maintain a consistent Result type. Note that get_location_info must return an APIError in its Err variant, so we must invoke Result.map_err in order to do type conversions, and use Result.map to ignore an unwanted intermediate value in favor of another (in our case, the body of a response, rather than the success result of having read it). This approach also suffers from an unfortunate functional failure in that it makes our code difficult to refactor. Suppose we wanted to not just return an Address in the Ok variant, but perhaps also the response's status code? I'll leave it as an exercise to the reader to figure out how we'd do that.

The use of method chaining is perhaps best left to shorter transformations where we might not mind losing information between each chained transformation.

Note: the following scores applies to the strategy of using this approach in the style featured in the example, and not the way I just recommended.

Effectiveness: 6 / 10

Elegance: 4 / 10

The try! Macro

The most popular way (which is no longer recommended, however) to handle most errors until now was to use the try! macro. Basically, try! is a simple macro that you can give an expression that evaluates to a Result which it will evaluate and match on. If the evaluated Result is the Ok variant, then the whole try! macro invocation will evaluate to the value contained within the Ok variant. If the variant is Err, then try! will cause the current function to immediately return with the whole Err variant and contained value. This means that instead of writing the kind of match expressions we saw earlier, we can instead write code like this.

let client = Client::new();
let mut response = try!(client.get("https://my.api.com/thing").send());
let mut body = String::new();
try!(response.read_to_string(&mut body));
let address = try!(json::decode<Address>(&body));

Not only will try! automatically return for us, it will even automatically perform the Err(error).map_err(From::from) error transformation to the Error type our function expects if std::convert::From is implemented for it!

While it may not look like it, this approach fundamentally works the same way that our method chaining example did. In both cases, we are repeatedly performing transformations on the value contained within Ok variants until we get an Err, in which case we immediately return it. This approach ends up saving a pretty hefty amount of code, and is substantially easier to understand. However, the try! macro has some stylistic faults worth noting. In particular, if you want to chain method calls on the returned Ok variant values at each step, you would have to write something like the following.

try!(
    try!(
        do_thing_one()
    )
    .do_thing_two()
)
.do_thing_three()

// Or
try!(try!(do_thing_one()).do_thing_two()).do_thing_three()

// Usually
let thing_one = try!(do_thing_one());
let thing_two = try!(thing_one.do_thing_two());
thing_two.do_thing_three()

Using the try! macro was the way to go for some time, but as of Rust 1.13.0's release, there's a better way. Of course, try! wouldn't be used in cases where you do not want to return an Err as soon as one's encountered, so it's technically not quite as versatile as explicitly matching.

Effectiveness: 8

Elegance: 7

The New ? Syntax

Nope, that's not a typo! As of Rust 1.13.0, a new piece of syntax, which is just the ? suffix for expressions, has landed in the language proper, rather than being a library feature like try!. It works like this. Where you would have previously wrapped an expression in try!, now you can suffix that expression with ? to have the exact same effect. So we could rewrite our example API invocation like so.

let client = Client::new();
let mut response = client.get("https://my.api.com/thing").send()?;
let mut body = String::new();
response.read_to_string(&mut body)?;
let address = json::decode<Address>(&body)?;

This example is certainly a little bit cleaner than before- it helps us focus on what the code is really doing rather than having to pay attention to the try! macro all the time- but it's really in cases where we want to do method chaining that it shines. Now, we can write code like this.

do_thing_one()
    ?.do_thing_two()
    ?.do_thing_three()

This, I think, is actually really fantastic! ? works just like the try! macro, so if we've implemented std::convert::From on our own Error type for all of the intermediate errors we might encounter, we don't have to explicitly call Result.map_err. This is absolutely the new recommended way to handle errors in cases where you want to immediately return the first Err encountered or otherwise obtain the value contained in Ok variants as you go.

Effectiveness: 8

Elegance: 9

Conclusion

A lot of the Rust code you are likely to find yourself writing will involve functions that return a Result. This will especially be the case when you are building or working with a library. In the majority of those cases, you're going to want to be performing transformations on the value contained within a Result's Ok variant, or else return the first Err you encounter, converted into the Error type that your codebase abstracts errors under. In those cases, you should prefer the ? syntax for its elegance. Of course, the other means of handling errors cannot be neglected. They still have their place. Perhaps you want to perform some transformation on the value contained in an Ok but don't want to have to store the intermediate value. In that case, it's probably most elegant to write the following.

let modified_value = do_some_thing().map(safely_manipulate)?;

Moreover, when you're implementing an application, your main function in particular is probably going to want to completely deconstruct the contents of some Result, since it certainly isn't going to propagate an error. For example, while you might have a function to load a configuration file into a Config struct that you've defined which will propagate any errors it encounters, your main function would probably want to check for an error and terminate the program if one occurred.

I hope that this post has been effective at introducing you to the most common ways to handle/propagate errors in Rust. The language features so many ways to deconstruct or else propagate Results precisely because there are so many ways that someone may want to address a potentially failed operation. It is very much worth your time to understand how pattern matching can be helpful to you, since both match and if let are useful for much more than just deconstructing errors. Likewise, it is worth your energy to try to design abstractions that propagate errors, and to understand when you should use the new ? syntax and when you might want to use Result.and_then, Result.map, Result.map_err and similar methods.

Finally, there is an excellent section in the Rust book on the subject of error handling, which goes into a lot more detail about some of the things I alluded to here. You should familiarize yourself with the content present there and may wish to return to this post until you find a style that works for you in the kinds of situations you deal with when writing Rust code.

Thank you so much for reading! As always, if you'd like to chat with me or share your thoughts about this article, please feel free to chat me up on Twitter!