Chapter 14

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.

A note on these tests and the filesystem

The test below writes a real file (test.txt) into the current working directory before it runs. 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
    
                
    Reveal the full solution Spoiler: the complete answer
    /// Adds two parsed numbers. Compare this to doing it with match statements.
    fn add_parsed_numbers(a: &str, b: &str) -> Result<i32, std::num::ParseIntError> {
        let a = a.parse::<i32>()?;
        let b = b.parse::<i32>()?;
        Ok(a + b)
    }
    
    #[test]
    fn test_add_parsed_numbers() {
        assert_eq!(add_parsed_numbers("10", "20"), Ok(30));
        assert!(add_parsed_numbers("abc", "10").is_err());
    }
    

    `?` 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
      
                  
      Reveal the full solution Spoiler: the complete answer
      /// Reads a file and counts lines. Note how `?` works with a different error type.
      fn count_file_lines(filename: &str) -> Result<usize, std::io::Error> {
          let content = std::fs::read_to_string(filename)?;
          Ok(content.lines().count())
      }
      
      #[test]
      fn test_count_file_lines() {
          use std::fs;
          fs::write("test.txt", "line 1\nline 2").unwrap();
          assert_eq!(count_file_lines("test.txt").unwrap(), 2);
          assert!(count_file_lines("missing.txt").is_err());
          fs::remove_file("test.txt").ok();
      }
      

      `?` through an iterator

      add_parsed_numbers propagated a single parse error. This step propagates a whole list of them.

      sum_numbers takes text with integers separated by whitespace and adds them up. The first token that isn't a number makes the function return that ParseIntError and stop. Because the function only parses (no file reading), one error type covers it: no boxing, no conversion.

      The interesting part is that ? rides straight through an iterator pipeline.

      Useful from the standard library

      • str::split_whitespace yields each token as a &str, skipping the gaps between numbers.
      • .map(|token| token.parse::<i32>()) turns each token into a Result<i32, ParseIntError>.
      • Iterator::sum has an impl that adds a sequence of Results: it returns the first Err, or the total wrapped in Ok. So .sum::<Result<i32, _>>()? collapses the whole list to an i32, or short-circuits on the first bad token.
      Exercise 3 of 3
      Open in Web Editor

      Results

        Compiler / runtime output
        
                    
        Reveal the full solution Spoiler: the complete answer
        /// Sums a whitespace-separated list of integers held in `text`.
        ///
        /// Each token is parsed with `?`: the first one that isn't a number
        /// short-circuits and returns its `ParseIntError`. The function only
        /// parses (no file I/O), so a single error type is enough and there's
        /// no need for `Box<dyn Error>`.
        fn sum_numbers(text: &str) -> Result<i32, std::num::ParseIntError> {
            let total: i32 = text
                .split_whitespace()
                .map(|token| token.parse::<i32>())
                .sum::<Result<i32, _>>()?;
            Ok(total)
        }
        
        #[test]
        fn test_sum_numbers() {
            assert_eq!(sum_numbers("5\n10\n15").unwrap(), 30);
            assert_eq!(sum_numbers("  1  2  3 ").unwrap(), 6);
            assert!(sum_numbers("5\nabc\n15").is_err()); // not a number
        }
        

        Wrapping up the `?` operator

        You replaced repetitive match chains with ?, propagated errors out of multi-step functions, and rode ? straight through an iterator pipeline.

        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.
        • ? composes nicely with iterator pipelines: a .parse() that returns Result slots straight in, and sum::<Result<_, _>>() short-circuits on the first error.
        • Every exercise here used a single error type, so ? propagated with no conversion. When a function genuinely mixes error types (say file I/O and parsing), you need a common error type. The env-file parser chapter picks that up with Box<dyn Error>.
        • 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 15Structs and methods