Item 3: Avoid matching Option and Result

Item 1 expounded the virtues of enum and showed how match expressions force the programmer to take all possibilities into account; this Item explores situations where you should prefer to avoid match expressions – explicitly at least.

Item 1 also introduced the two ubiquitous enums that are provided by the Rust standard library:

  • Option<T> to express that a value (of type T) may not be present
  • Result<T, E>, for when an operation to return a value (of type T) may not succeed, and may instead return an error (of type E).

For these particular enums, explicitly using match often leads to code that is less compact than it needs to, and which isn't idiomatic Rust.

The first situation where a match is unnecessary is when only the value is relevant, and the absence of value (and any associated error) can just be ignored.

    struct S {
        field: Option<i32>,
    }

    match &s.field {
        Some(i) => println!("field is {}", i),
        None => {}
    }

For this situation, an if let expression is one line shorter and, more importantly, clearer:

    if let Some(i) = &s.field {
        println!("field is {}", i);
    }

However, most of the time the absence of a value, and an associated error, is going to be something that the programmer has to deal with. Designing software to cope with failure paths is hard, and most of that is essential complexity that no amount of syntactic support can help with – deciding what should happen if an operation fails.

In some situations, the right decision is to perform an ostrich manoeuvre and explicitly not cope with failure. Doing this with an explicit match would be needlessly verbose:

    let result = std::fs::File::open("/etc/passwd");
    let f = match result {
        Ok(f) => f,
        Err(_e) => panic!("Failed to open /etc/passwd!"),
    };

Both Option and Result provide a pair of methods that extract their inner value and panic! if it's absent: unwrap and expect. The latter allows the error message on failure to be personalized, but in either case the resulting code is shorter and simpler – error handling is delegated to the .unwrap() suffix (but is still present).

    let f = std::fs::File::open("/etc/passwd").unwrap();

Be clear, though: these helper functions still panic!, so choosing to use them is the same as choosing to panic! (Item 17).

However, in many situations, the right decision for error handling is to defer the decision to somebody else. This is particularly true when writing a library, where the code may be used in all sorts of different environments that can't be foreseen by the library author. To make that somebody else's job easier, prefer Result to Option, even though this may involve conversions between different error types (Item 4); Result has also a [#must_use] attribute to nudge library users in the right direction.

Explicitly using a match allows an error to propagate, but at the cost of some visible boilerplate (reminiscent of Go):

    pub fn find_user(username: &str) -> Result<UserId, std::io::Error> {
        let f = match std::fs::File::open("/etc/passwd") {
            Ok(f) => f,
            Err(e) => return Err(e),
        };

The key ingredient for reducing boilerplate is Rust's question mark operator ?. This piece of syntactic sugar takes care of matching the Err arm and the return Err(...) expression in a single character:

    pub fn find_user(username: &str) -> Result<UserId, std::io::Error> {
        let f = std::fs::File::open("/etc/passwd")?;

Newcomers to Rust sometimes find this disconcerting: the question mark can be hard to spot on first glance, leading to disquiet as to how the code can possibly work. However, even with a single character, the type system is still at work, ensuring that all of the possibilities expressed in the relevant types (Item 1) are covered – leaving the programmer to focus on the mainline code path without distractions.

What's more, there's generally no cost to these apparent method invocations: they are all generic functions marked as [#inline], so the generated code will typically compile to machine code that's identical to the manual version.

These two factors taken together mean that you should prefer Option and Result transforms to explicit match expressions.

In the previous example, the error types lined up: both the inner and outer methods expressed errors as std::io::Error. That's often not the case; one function may accumulate errors from a variety of different sub-libraries, each of which uses different error types. Error mapping in general is discussed in Item 4; for now, just be aware that a manual mapping:

    pub fn find_user(username: &str) -> Result<UserId, String> {
        let f = match std::fs::File::open("/etc/passwd") {
            Ok(f) => f,
            Err(e) => {
                return Err(format!("Failed to open password file: {:?}", e))
            }
        };
        // ...

can be more succinctly and idiomatically expressed with the .map_err() transformation:

    pub fn find_user(username: &str) -> Result<UserId, String> {
        let f = std::fs::File::open("/etc/passwd")
            .map_err(|e| format!("Failed to open password file: {:?}", e))?;
        // ...

This approach generalizes more widely. The question mark operator is a big hammer; use transformation methods on Option and Result types to manoeuvre them into a position where they can be a nail.

The standard library provides a wide variety of these transformation methods to make this possible, as shown in the following map. In line with Item 17, methods that can panic are highlighted in red.

Option/Result transformations

(The online version of this diagram is clickable: each box links to the relevant documentation.)

One common situation isn't covered by the diagram is dealing with references. For example, consider a structure that optionally holds some data.

    struct InputData {
        payload: Option<Vec<u8>>,
    }

A method on this struct which tries to pass the payload to an encryption function with signature (&[u8]) -> Vec<u8> fails if there's a naive attempt to take a reference:

    impl InputData {
        pub fn encrypted(&self) -> Vec<u8> {
            encrypt(&self.payload.unwrap_or(vec![]))
        }
    }
error[E0507]: cannot move out of `self.payload` which is behind a shared reference
  --> transform/src/main.rs:57:22
   |
57 |             encrypt(&self.payload.unwrap_or(vec![]))
   |                      ^^^^^^^^^^^^
   |                      |
   |                      move occurs because `self.payload` has type `Option<Vec<u8>>`, which does not implement the `Copy` trait
   |                      help: consider borrowing the `Option`'s content: `self.payload.as_ref()`

The error message describes exactly what's needed to make the code work, the as_ref() method1 on Option. This method converts a reference-to-an-Option to be an Option-of-a-reference:

        pub fn encrypted(&self) -> Vec<u8> {
            encrypt(self.payload.as_ref().unwrap_or(&vec![]))
        }

To sum up:

  • Get used to the transformations of Option and Result, and prefer Result to Option.
    • Use .as_ref() as needed when transformations involve references.
  • Use them in preference to explicit match operations.
  • In particular, use them to transform result types into a form where the ? operator applies.


1: Note that this method is separate from the AsRef trait, even though the method name is the same.