Chapter 14

The ? Operator

"Handle all errors" is great advice! ...unless you end up with boilerplate like this:

fn is_even(text: &str) -> Result<bool, std::num::ParseIntError> {
    let number = match text.parse::<i32>() {
        Ok(n) => n,
        Err(e) => return Err(e),
    };
    Ok(number % 2 == 0)
}

The ? operator is shorthand for that pattern. Put it after a Result expression to get the value from Ok and continue. If the result is Err, the function returns the error immediately.

fn is_even(text: &str) -> Result<bool, std::num::ParseIntError> {
    let number = text.parse::<i32>()?;
    Ok(number % 2 == 0)
}

For these exercises, ? on a Result needs a function that also returns a Result. It also works on Option inside functions returning Option, where None causes an early return. To use ? on a Result in main, give main a Result return type.

`?` 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."

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.

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.
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 needs the surrounding function to return the same error type, or one it can convert into with From. When ? fails to compile, I would check those two error types first. Working out the conversion can take a few tries; this exercise avoids that by using std::io::Error throughout.

    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.

    The Test and the Filesystem

    This test creates test.txt in the current working directory and removes it afterward. It also expects missing.txt not to exist. Avoid running multiple copies of this test in the same directory: they can overwrite or remove each other's file. For tests in one process that share a path, cargo test -- --test-threads=1 prevents them from running at the same time. Separate test processes still need separate paths.

    In production tests, tempfile::NamedTempFile creates a uniquely named file and removes it when dropped.

    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();
      }
      

      `?` inside a Loop

      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.

      Use a for loop and apply ? to each token's parse result. What happens to the rest of the loop when parsing fails? Return Ok(0) for empty or whitespace-only input. Assume the running total fits in an i32.

      Useful from the Standard Library

      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 mut total = 0;
            for token in text.split_whitespace() {
                total += token.parse::<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
        }
        
        #[test]
        fn test_sum_numbers_empty_and_signed() {
            assert_eq!(sum_numbers(""), Ok(0));
            assert_eq!(sum_numbers(" \t\n"), Ok(0));
            assert_eq!(sum_numbers("-5 +2 3"), Ok(0));
        }
        
        #[test]
        fn test_sum_numbers_first_error() {
            let first_error = "abc".parse::<i32>().unwrap_err();
            assert_eq!(sum_numbers("5 abc 99999999999999999999"), Err(first_error));
        }
        

        Wrapping Up the ? Operator

        You replaced repetitive match chains with ?, propagated errors out of multi-step functions, and used ? inside a loop.

        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).
        • For Result, the surrounding function must return a Result with the same error type, or one the error converts into via From. For Option, the surrounding function returns Option; there is no error value to convert.
        • Inside a loop, a failed ? returns from the whole function, not just the current iteration. Later tokens are never parsed.
        • Every exercise here used a single error type, so ? propagated with no conversion. When a function 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 share a filesystem path can race when they run in parallel. Use separate paths; --test-threads=1 only serializes tests within one test process.
        Next chapter 15Structs and Methods

        Optional Chapters

        Extra practice to explore at your own pace. These chapters do not count toward course progress.

        ← Return to the Rust course curriculum