Chapter 19

The `?` Operator

👋 Anyone can read and edit this exercise. Sign up to save your progress.

Working with Result quickly becomes verbose if every call needs a match-then-return:

fn parse_two(a: &str, b: &str) -> Result<i32, std::num::ParseIntError> {
    let x = match a.parse::<i32>() {
        Ok(n) => n,
        Err(e) => return Err(e),
    };
    let y = match b.parse::<i32>() {
        Ok(n) => n,
        Err(e) => return Err(e),
    };
    Ok(x + y)
}

The ? operator is shorthand for that pattern. Slap it onto any Result expression: if it's Ok, the value is unwrapped and execution continues; if it's Err, the function returns the error immediately.

fn parse_two(a: &str, b: &str) -> Result<i32, std::num::ParseIntError> {
    let x = a.parse::<i32>()?;
    let y = b.parse::<i32>()?;
    Ok(x + y)
}

? only works inside a function whose return type is also a Result (or Option). It doesn't work in fn main() unless main itself returns a Result.

Mixing error types

What if one call returns ParseIntError and another returns io::Error? The compiler needs them to agree. The two common solutions:

  1. Box<dyn Error>: a trait-object error type that accepts almost any error. Quick and cheap, great for small programs and prototypes.

    fn run() -> Result<i32, Box<dyn std::error::Error>> {
        let text = std::fs::read_to_string("nums.txt")?;   // io::Error
        let n: i32 = text.trim().parse()?;                 // ParseIntError
        Ok(n * 2)
    }
    

    This is where Box<dyn Error> finally pays off. You met both halves of that signature in the previous two chapters: Box<T> is the heap-owning smart pointer from chapter 15, and dyn Error is the trait-object syntax from chapter 14 ("any type that implements the Error trait"). Putting them together gives you "a heap- allocated, any-error-goes return type" that ? can convert into from almost any concrete error.

  2. A custom error enum with a variant per underlying error. More work, but the type system tells callers exactly what can go wrong. You'll see the thiserror crate used for this in real projects.

? knows how to convert between error types if there's a From implementation. Box<dyn Error> works because nearly every error type has From<E> for Box<dyn Error>.

A note on these tests and the filesystem

The tests below write real files (test.txt, numbers.txt, …) into the current working directory before they run. Cargo runs tests in parallel by default, so two tests writing to the same path can race each other and cause spurious failures. If you see flaky Errs here, force the harness to run them one at a time:

cargo test -- --test-threads=1

Or give each test its own filename if you're feeling tidy. In production code you'd reach for tempfile::NamedTempFile so the OS hands you a guaranteed-unique path and cleans up after itself.

`?` for the simple case

? is Rust's shortcut for "if this is Err, return it from the current function; otherwise, unwrap the value and continue." It compresses a lot of match boilerplate into one character.

Here both calls to parse() return the same error type (ParseIntError), so ? works directly without any conversion. Compare to writing this out with a match on each parse() result. That's the boilerplate ? is replacing.

Useful from the standard library

  • str::parse returns Result<T, T::Err>. Combined with ? you get the parsed number on the happy path and an early-return on failure.
  • std::num::ParseIntError is the error type for integer parses. The function signature declares it directly, so ? doesn't need to convert anything.
  • The function returns one expression: Ok(a.parse::<i32>()? + b.parse::<i32>()?). Each ? unwraps an integer, then + adds them, then Ok(...) wraps the sum back up.
Exercise 1 of 3
Open in Web Editor

Results

    Compiler / runtime output
    
                

    `?` with a different error type

    Same operator, different error type. File I/O returns std::io::Error; the function signature has to declare it as the error type so ? is happy passing it through.

    Notice that ? doesn't care which error type is involved as long as the function it's used in returns the same error type (or one convertible from it via From, which is the next step).

    Useful from the standard library

    • std::fs::read_to_string reads the whole file into a String. Returns Result<String, io::Error>, which is exactly what ? wants.
    • str::lines iterates over the file's lines without keeping the trailing newlines.
    • Iterator::count consumes the iterator and returns how many lines there were.
    • The full body fits on one line: Ok(std::fs::read_to_string(filename)?.lines().count()).
    Exercise 2 of 3
    Open in Web Editor

    Results

      Compiler / runtime output
      
                  

      `?` across multiple error types

      Now the function combines two distinct error sources: parsing a string into a number (ParseIntError) and reading a file (std::io::Error). With a fixed error type you'd need to convert each one by hand. Box<dyn std::error::Error> is the laziest catch-all: any type that implements the Error trait can be boxed into it, and ? does the conversion automatically because both ParseIntError and io::Error implement Error.

      In real code you'd typically define your own error enum (or reach for a crate like anyhow or thiserror). Box<dyn Error> is the standard-library escape hatch.

      Useful from the standard library

      • std::fs::read_to_string reads the file. Use ? to propagate the I/O error.
      • str::lines walks the lines. Pair with str::trim to drop stray whitespace before parsing.
      • For the per-line parse, an iterator pipeline with .map(|l| l.parse::<i32>()).sum::<Result<i32, _>>() collects the total in one call: sum on a Result-yielding iterator returns the first error or the total. The ? after that reduces it back to i32.
      • Returning Box<dyn std::error::Error> works because both error types implement the Error trait and ? knows how to box them.
      Exercise 3 of 3
      Open in Web Editor

      Results

        Compiler / runtime output
        
                    

        Wrapping up the `?` operator

        You replaced repetitive match chains with ?, used it across matching error types, then mixed two error sources in one function via Box<dyn Error>.

        What we learned

        • ? is shorthand for "if this is Err, return it from the current function; if it's Ok, unwrap the value and keep going." It works on Option too (returning None early).
        • The function using ? must return a Result (or Option) whose error type matches, or one that the failing error converts into via From.
        • When all calls produce the same error type, ? just propagates. When they don't, you need a common error type. The two standard options are Box<dyn std::error::Error> (cheap, lossy on type info) and a custom enum (more work, more clarity).
        • ? chains compose nicely with iterator pipelines: a .parse() that returns Result slots straight in, and sum::<Result<_, _>>() short-circuits on the first error.
        • For real applications the anyhow and thiserror crates are the usual upgrades over Box<dyn Error> and hand-rolled enums respectively.
        • Tests that touch the filesystem can race when the harness runs in parallel. Use unique filenames or cargo test -- --test-threads=1 if you see flaky failures.
        Next chapter 20Modules and Visibility