Result and the ? operator

Rust · Practical · 12 min

Every I/O, parsing and network call returns Result; handling it fluently is the core of practical Rust.

Rust separates two kinds of failure. Unrecoverable errors are bugs (an index out of range, a broken invariant) and trigger a panic!, which unwinds the thread. Recoverable errors, like a missing file or bad input, are ordinary values of type `Result<T, E>`, an enum with Ok(T) and Err(E).

You can match on a Result, but the idiomatic way to pass errors up is the `?` operator. Inside a function that returns a Result, let text = fs::read_to_string(path)?; means: if Ok, unwrap the value; if Err, return it from the current function immediately. Before returning, ? converts the error with From::from, so a function returning Result<T, MyError> can use ? on any error type that MyError implements From for. ? also works on Option in functions returning Option.

main can return Result<(), E> where E: Debug; returning Err prints the error and exits with a non-zero code. A convenient quick signature is fn main() -> Result<(), Box<dyn std::error::Error>>, since any standard error converts into that box.

Useful methods: map_err to change the error type, ok() to turn it into an Option, unwrap_or_else, and_then, and is_ok()/is_err(). unwrap() and expect() panic on Err.

Result is marked #[must_use], so ignoring one produces a warning. If you really do not care, write let _ = do_thing(); to show it was deliberate.

A good rule: library code returns Result and lets the caller decide; only application code at the top decides to print, retry or exit.

Example

use std::num::ParseIntError;

fn parse_pair(s: &str) -> Result<(i32, i32), String> {
    let (a, b) = s.split_once(',').ok_or("missing comma")?;
    let a: i32 = a.trim().parse().map_err(|e: ParseIntError| e.to_string())?;
    let b: i32 = b.trim().parse().map_err(|e: ParseIntError| e.to_string())?;
    Ok((a, b))
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    println!("{:?}", parse_pair("3, 4"));  // Ok((3, 4))
    println!("{:?}", parse_pair("3 4"));   // Err("missing comma")
    println!("{:?}", parse_pair("3, x"));  // Err("invalid digit found in string")

    let n: u8 = "200".parse()?;            // ParseIntError converts into Box<dyn Error>
    println!("n = {n}");
    Ok(())
}

Remember

  • Panics are for bugs; Result is for failures the caller should handle
  • ? returns early on Err and converts the error with From
  • main can return Result to use ? at the top level

Watch out for

  • Using .unwrap() on every Result in library code, turning recoverable errors into crashes.

Check yourself

  1. In `fn load() -> Result<Config, AppError>`, what lets `std::fs::read_to_string(p)?` compile?
  2. What does `?` do when applied to `Ok(v)`?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html