Rust from the ground up

Errors you can recover from

Result instead of exceptions, unwrap and expect and when to avoid them, the question mark that passes errors up a level, and the line between a bug and a bad day.

Part 8 of 12

Intermediate18 min readBack to Courses

panic or Result

Rust divides trouble in two. A panic! is a bug — an array index that cannot exist, a broken assumption — and it stops the program with a message. A Result<T, E> is a bad day the program is expected to survive: a file that is missing, a line of text that is not a number.

Result is an enum: Ok(value) when it worked, Err(error) when it did not. Nothing throws, nothing is caught, and a function that can fail says so in its signature — so the caller knows before reading the body.

Handling or passing it on

The honest handling is a match: one arm for Ok, one for Err, and the error is a value you can log, retry, or turn into a message. unwrap() and expect("message") are the shortcuts — they panic on an Err — and they are right in examples and in tests, and wrong in a program that should keep running.

The ? operator is the middle path: after a call that returns a Result, ? returns the error to your caller immediately if there was one, and gives you the value if there was not. It only works in a function whose own return type allows that error, which is why error handling in Rust is visible in the signature and nowhere else.

It is not only for Result. In a function that returns Option<T>, ? works on an Option too, returning None early in the same way. And when two functions fail with different error types, map_err converts on the way through: .parse().map_err(|_| ShopError::BadQuantity(n))?. The conversion is written out because the program is choosing which of its failures this one is, rather than collapsing everything into a string.

Where the message comes from

Every error type can describe itself; the standard ones are already good at it, and printing an Err with {:?} gives the detail. In bigger programs you will define an enum of your own for the failures the program can have — one option per failure — and implement the display of it, so a user reads a sentence and a log gets the structure.

That is also how ? stays useful: it forwards whatever error type the function declared, so a chain of three fallible calls needs one ? each and no nested matches.

From unwrap to a real signature

fn add_twice(text: &str) -> Result<i32, std::num::ParseIntError> {
    let n: i32 = text.trim().parse()?;  // ? returns early on failure
    Ok(n + n)
}

fn main() {
    let good = add_twice(" 21 ");
    println!("good: {:?}", good);

    let bad = add_twice("twenty-one");
    match bad {
        Ok(n) => println!("worked: {}", n),
        Err(error) => println!("that was not a number: {}", error),
    }

    // The shortcut, for when a panic is the right answer
    let sure: i32 = "7".parse().expect("seven should parse");
    println!("sure: {}", sure);
}

A parser that survives bad lines

Run it, then add a line "twelve" to the list and watch the error path. Then change the program to keep the good values and report how many lines failed.

Output
Click "Run" to execute your code

unwrap() in a loop over user input is how programs die on an empty line. If the input comes from outside your program, it can be wrong — that is the definition — so the type of the failure should be in the signature, not in a panic.

An error type of your own

A Result is only as useful as its error type. ParseIntError says what went wrong but not what the program was trying to do. An enum with one option per failure the program can have — OutOfStock, BadQuantity — names the cases the caller must handle, and the compiler checks that they are handled.

A good error type implements two traits first: Display for the sentence a reader sees, and Debug for the structure a log keeps. Display is what {} calls. To use the type as a Box<dyn Error>, implement Error too, which has no methods of its own and takes one line.

Option is not a small Result

Option says a value might be absent; Result says an operation might fail and, crucially, why. A missing key in a map is an Option, because absence is normal there. A missing file when the user asked to open one is a Result, because the caller needs to know what went wrong and say so.

Converting between them is explicit: option.ok_or(error) turns a None into an Err, and result.ok() throws the reason away. The conversion is worth pausing over, because it is exactly where a program decides whether a failure deserves an explanation.

Three error-handling slips. ? in main needs main to return a Result, so either change the signature or handle the value. let _ = ... on a Result silently discards a failure — fine when you truly do not care, a bug when you do. And matching on the text of an error message is fragile; match on the variant and let Display produce the sentence.

Try these next

  • Add a TooMany(u32) option to the shop error, return it above ten items, and give it its own sentence in Display.
  • Change order to take a &str price and parse it, so the function has two different error types to report.
  • Write a second function that returns Option<&str> for an unknown item, then convert its None into your error with ok_or.

A shop that explains its refusals

Run it and read the three answers. Then add a fourth attempt with a quantity of 12 against a stock of 10, and decide whether it should be OutOfStock or a new error of its own.

Output
Click "Run" to execute your code

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What are the two options of a Result<T, E>?