Flashcard Rust: Result Type in Rust and how to handle it

A worked Rust flashcard about Result values, pattern matching, the question-mark operator, unwrap, and propagating recoverable errors.
coding
rust
Author

unrahul

Published

January 14, 2021

Result is how Rust lets a function return either a useful value or a useful error. The part I had to get comfortable with was that receiving a Result means I still have a decision to make.

Flashcard 04 Rust

The question

What should I do with a Result<T, E>?

The value can contain success or failure. The caller decides what happens next.

Reveal the answer

Match Ok and Err, propagate the error with ?, or use unwrap only when a panic is acceptable.

What a Result contains

The type has two variants:

enum Result<T, E> {
    Ok(T),
    Err(E),
}

Ok(T) carries the success value. Err(E) carries the error value. Both types are generic, so a function chooses what success and failure mean for its own job.

Here is a deliberately small example:

fn is_even(number: u32) -> Result<String, String> {
    if number % 2 == 0 {
        Ok(format!("{number} is even!"))
    } else {
        Err(format!("{number} is not even!"))
    }
}

For this function, both variants contain a String. The variant itself tells me whether the operation succeeded.

Why unwrap can panic

This is the shortest way to get the success value:

fn main() {
    let message = is_even(2).unwrap();
    println!("{message}");
}
2 is even!

But unwrap does not handle the error case. It panics if the value is Err:

fn main() {
    let message = is_even(1).unwrap();
    println!("{message}");
}
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value:
"1 is not even!"'

The tradeoff with unwrap

unwrap is convenient when a failure really should stop the program, or while I am writing a quick experiment. It is a poor substitute for deciding how a real error should be handled.

Propagate the error with question mark

The ? operator extracts the value from Ok. If it sees Err, it returns that error from the current function:

fn print_even(number: u32) -> Result<(), String> {
    let message = is_even(number)?;
    println!("{message}");
    Ok(())
}

fn main() -> Result<(), String> {
    print_even(2)?;
    Ok(())
}

The function using ? must return a compatible error type. Here both print_even and main return Result<_, String>, so the error can move up the call stack unchanged.

Handle both variants with match

When I want different behavior for success and failure, match makes both paths explicit:

fn main() {
    match is_even(1) {
        Ok(message) => println!("{message}"),
        Err(error) => println!("Could not continue: {error}"),
    }
}
Could not continue: 1 is not even!

No panic, and no error path hidden from the reader. This is more typing than unwrap, but it gives me full control.

What I keep in my head

Use match when both paths need behavior, ? when the caller should handle the error, and unwrap only when stopping immediately is the intended behavior.