OwenGage.com: writing

How to think of unwrap

2021-08-30

Short summary

The unwrap method can be confusing to newcomers. Some advice:

  • expect(&str) can be used rather than unwrap() to provide context for a panic.
  • Use unwrap and expect like assertions. If they panic it's only in unrecoverable situations.
  • Avoid usage in library code.
  • Leave a comment to explain why its okay.

If you read on you'll get more context for the advice, and examples. Even the standard library panics when it could in theory recover.

What is unwrap?

Rust does not have exceptions. This typically means that fallible functions return a Result<T> or Option<T> to indicate when they have failed. .unwrap() allows you to get the Ok or Some value inside, if it exists.

Let's look at std::str::from_utf8 from the standard library:

pub fn from_utf8(v: &[u8]) -> Result<&str, Utf8Error>
let s = std::str::from_utf8(b"hello, world").unwrap();
println!("{}", s);

Here we use .unwrap() in order to get the value inside the Result. If it turns out to not be valid UTF-8 (str has to be UTF-8), then we end up panicking:

let s = std::str::from_utf8(&[255]).unwrap();
println!("{}", s);
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Utf8Error { valid_up_to: 0, error_len: Some(1) }', src/main.rs:2:45
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

When to use unwrap

So when should you use .unwrap() in your code?

  • When you know better than the compiler.
  • When you don't care if some code panics.
  • When you have no expectation of recovering from the error.
  • In test code.

You know better

Sometimes you know better than the compiler. In our first example we convert the byte literal b"hello, world" into a string. We as the programmer can obviously see that it is valid UTF-8, so the unwrap will never panic.

When it isn't so obvious (and even when it is), you should leave a comment explaining why the unwrap will not panic.

Here's an example from the regex crate:

fn clear_cache_and_save(
    &mut self,
    current_state: Option<&mut StatePtr>,
) -> bool {
    if self.cache.compiled.is_empty() {
        // Nothing to clear...
        return true;
    }
    match current_state {
        None => self.clear_cache(),
        Some(si) => {
            let cur = self.state(*si).clone();
            if !self.clear_cache() {
                return false;
            }
            // The unwrap is OK because we just cleared the cache and
            // therefore know that the next state pointer won't exceed
            // STATE_MAX.
            *si = self.restore_state(cur).unwrap();
            true
        }
    }
}

The big comment for the unwrap sticks out quite a bit. It explains why it is fine in this case, though it may require some understanding of the surrounding code to understand.

Another time you would use it is for quick and dirty code that maybe you are writing/using once, or don't care about panics. Using it for examples is probably acceptable too.

You should avoid unwrap when writing library code. Even when you are certain it is valid now, it may not be with further code changes. At the very least you should have good test coverage for that area of code. The performance penalty of handling the failure, using match or the ? operator, is usually small.

If you're worried about the performance impact you should benchmark your code to see how big the impact is. You can make a judgement from there.

No expectation of recovery

There are situations in which you might not want to handle rare or near-impossible edge cases.

Some examples are:

  • Mutex poisoning.
  • Thread creation failure.
  • Allocation failure.

These usually represent serious problems happening elsewhere. Mutex poisoning happens when a panic occurs while holding a lock on a mutex (maybe because of a bad unwrap!). It's possible to handle this error, but typically code will just call unwrap on the result of Mutex::lock() because there is often nothing sensible to do, other than propagate the panic and terminate.

There are situations even standard library itself considers beyond scope to handle:

  • std::thread::spawn panics if the underlying OS fails to make a thread. You need to use the Builder to handle this type of failure.

  • Box, Vec and other allocating code assume memory allocations cannot fail. There are good reasons for this in userspace code, but it has hindered Rust's adoption in some areas, like the Linux kernel or cURL.

In tests

You can think of unwrap like an assertion. You use it when you are certain it is fine, or that a situation is unrecoverable if not. This means it is also quite suitable for using in tests. Here is a test from fastnbt:

#[test]
fn simple_byte() {
    #[derive(Deserialize)]
    struct V {
        abc: i8,
    }

    let payload = Builder::new()
        /* snip */
        .build();

    let v: V = from_bytes(payload.as_slice()).unwrap();

    assert_eq!(v.abc, 123);
}

You can see on the from_bytes line that I just unwrap the result. If the result is an error then this is a test failure and it causes a panic, failing the test like we want.

The expect method

The expect function is like unwrap but takes a &str argument allowing the programmer to explain why this can never fail.

This acts similar to a comment in the source code, and also prints out the message given along with the panic:

thread 'main' panicked at 'should be ascii: Utf8Error { valid_up_to: 0, error_len: Some(1) }', src/main.rs:2:45
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

This can make it a little clearer what has gone wrong. An explanation of why something cannot fail is usually awkwardly long for a string argument, and isn't massively useful to the user who had their code panic anyway. So a longer comment alongside the expect is a good idea.

Closing

Hopefully this clears up when to use unwrap and expect. They are pretty simple functions, but it can be unclear what idiomatic usage is.

If there are any other areas of Rust that you find trip up newcomers I'd love suggestions for further articles. You can find me on twitter @owengagecom.