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

    Here you use the same operator with a different error type. File I/O returns std::io::Error, and this function declares that same error type so ? can pass failures back unchanged.

    ? doesn't care which concrete error type is involved. It only needs the surrounding function to return the same error type, or one it can convert into with From.

    Useful from the standard library

    • std::fs::read_to_string reads the whole file into a String. The returned Result<String, io::Error> matches this function's error type.
    • 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.
    • Once you have the file contents, lines() and count() can remain in the same expression.
    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 had two chances to return a parse error. sum_numbers may inspect many tokens, but it still returns only the first parse error it encounters.

      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. Since the function only parses, one error type covers every failure without boxing or conversion.

      Here the iterator pipeline produces one Result, and ? either unwraps its total or returns the first error.

      Useful from the standard library

      • str::split_whitespace yields each token as a &str, skipping the gaps between numbers.
      • Parsing each token turns the iterator into a sequence of Result<i32, ParseIntError> values.
      • Iterator::sum can add that sequence of Results, returning the first Err or the total wrapped in Ok. Once sum has collapsed those results, ? gives you the total on success.
      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 used ? after 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.
        • ? can follow an iterator pipeline that produces a Result, so the first error still returns early.
        • 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 uses Box<dyn Error> as a common error type.
        • 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