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.
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.
? 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::parsereturnsResult<T, T::Err>. Combined with?you get the parsed number on the happy path and an early-return on failure.std::num::ParseIntErroris 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, thenOk(...)wraps the sum back up.
/// 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());
}
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_stringreads the whole file into aString. ReturnsResult<String, io::Error>, which is exactly what?wants.str::linesiterates over the file's lines without keeping the trailing newlines.Iterator::countconsumes 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()).
/// 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();
}
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_whitespaceyields each token as a&str, skipping the gaps between numbers..map(|token| token.parse::<i32>())turns each token into aResult<i32, ParseIntError>.Iterator::sumhas an impl that adds a sequence ofResults: it returns the firstErr, or the total wrapped inOk. So.sum::<Result<i32, _>>()?collapses the whole list to ani32, or short-circuits on the first bad token.
/// 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
}
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 isErr, return it from the current function; if it'sOk, unwrap the value and keep going." It works onOptiontoo (returningNoneearly).- The function using
?must return aResult(orOption) whose error type matches, or one that the failing error converts into viaFrom.?composes nicely with iterator pipelines: a.parse()that returnsResultslots straight in, andsum::<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 withBox<dyn Error>.- Tests that touch the filesystem can race when the harness runs in parallel. Use unique filenames or
cargo test -- --test-threads=1if you see flaky failures.