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());
}
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_stringreads the whole file into aString. The returnedResult<String, io::Error>matches this function's error type.str::linesiterates over the file's lines without keeping the trailing newlines.Iterator::countconsumes the iterator and returns how many lines there were.- Once you have the file contents,
lines()andcount()can remain in the same expression.
/// 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 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_whitespaceyields 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::sumcan add that sequence ofResults, returning the firstError the total wrapped inOk. Oncesumhas collapsed those results,?gives you the total on success.
/// 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 used ? after 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.?can follow an iterator pipeline that produces aResult, 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 usesBox<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=1if you see flaky failures.