12.3Propagating errors: the ? operator
A function that does several fallible things in a row, parse this, then parse that, then combine them, faces the same chore at every step: check the Result, and if it's an Err, stop and hand the error back to the caller. Written with match, this buries the actual logic under failure-handling. The ? operator exists to make it vanish. It is one of the most-loved pieces of Rust syntax, and by the end of this lesson you'll see why.
The problem: match pyramids
Here's a function that parses two strings and adds them, handling failure properly with match:
fn add_strings(a: &str, b: &str) -> Result<i32, std::num::ParseIntError> {
let first = match a.parse::<i32>() {
Ok(n) => n,
Err(e) => return Err(e),
};
let second = match b.parse::<i32>() {
Ok(n) => n,
Err(e) => return Err(e),
};
Ok(first + second)
}
fn main() {
println!("{:?}", add_strings("3", "4")); // Ok(7)
println!("{:?}", add_strings("3", "oops")); // Err(...)
}Ok(7)
Err(ParseIntError { kind: InvalidDigit })
It works, but look at the shape. Each parse is wrapped in a match whose only job is "on Ok, keep the value; on Err, return it to the caller." That pattern, propagating the error upward, is so common that drowning the real work (first + second) in it is a genuine readability problem. The two match blocks say nothing interesting; they're pure plumbing.
The fix: ?
The ? operator is that exact pattern, as one character. Place ? after an expression that returns a Result, and it does this: if the value is Ok(v), it evaluates to v and execution continues; if the value is Err(e), it returns Err(e) from the enclosing function immediately. The same function, rewritten:
fn add_strings(a: &str, b: &str) -> Result<i32, std::num::ParseIntError> {
let first = a.parse::<i32>()?;
let second = b.parse::<i32>()?;
Ok(first + second)
}
fn main() {
println!("{:?}", add_strings("3", "4")); // Ok(7)
println!("{:?}", add_strings("3", "oops")); // Err(...)
}Ok(7)
Err(ParseIntError { kind: InvalidDigit })
Identical behavior, and now the logic is visible. a.parse::<i32>()? means "parse, and if it failed, return that error from add_strings right now; otherwise give me the number." The two ?s replace the two match blocks exactly, and the happy path reads like there were no errors to worry about at all, while every failure still gets propagated faithfully. This before-and-after is the whole lesson: ? is sugar for "match, and return Err on failure."
Key insight
? is early-return-on-error in one character. expr? unwraps an Ok to its value or returns the Err from the current function. It turns the question "did this fail?" from something you write out at every step into something the ? asks for you, so the function body shows the success path plainly and the errors route themselves upward. This is why idiomatic Rust is full of ? and nearly free of match-on-Result plumbing.
? only works in functions that return Result
There's a catch that the compiler enforces, and it follows directly from what ? does. If ? returns an Err from the current function, that function must be able to return an Err, which means its return type must be a Result (or Option, or another compatible type). Use ? in a function that returns plain i32 and the compiler stops you:
fn bad(a: &str) -> i32 {
let n = a.parse::<i32>()?;
n * 2
}error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
--> src/main.rs:2:30
|
1 | fn bad(a: &str) -> i32 {
| ---------------------- this function should return `Result` or `Option` to accept `?`
2 | let n = a.parse::<i32>()?;
| ^ cannot use the `?` operator in a function that returns `i32`
The fix is to make bad return Result<i32, ...>. The error message even points at the signature as the thing to change. This isn't ? being fussy; it's the type system insisting that "this function can produce an error" be written in the function's type, the same honesty Result brought in the first place.
main can return Result
This raises a question: the error has to go somewhere. Each function passes it to its caller, but what about main, which has no caller? You can let main itself return a Result, and then ? works at the top level too:
use std::num::ParseIntError;
fn main() -> Result<(), ParseIntError> {
let n: i32 = "42".parse()?;
println!("parsed {n}");
Ok(())
}parsed 42
A main returning Result<(), E> succeeds with Ok(()) (the unit () from lesson 1.11, meaning "nothing to return, it just worked") and may use ? in its body. If main returns an Err, the program prints the error's debug form and exits with a failure code (lesson 7.8). This is the clean alternative to unwrap in main: instead of panicking on the first error, main can ?-propagate it and let the runtime report it. We'll lean on this in the next lesson.
Tip
? also works on Option: expr? returns None early if expr is None, or unwraps the Some. It's the same idea applied to absence instead of failure, and it only works in a function returning Option. So ? is the universal "unwrap or bail to the caller" operator for both of chapter 11's fallible enums.
Quiz time
Question #1
In one sentence, what does expr? do when expr is Ok(v), and what does it do when expr is Err(e)?
Show solution
When expr is Ok(v), expr? evaluates to v and execution continues; when expr is Err(e), expr? returns Err(e) from the enclosing function immediately. It's unwrap-or-return-the-error.
Question #2
Why won't this compile, and what's the fix?
fn first_number(text: &str) -> i32 {
let n = text.parse::<i32>()?;
n
}Show solution
? can only be used in a function that returns Result (or Option), because on Err it returns the error from the function, and i32 can't hold an error (E0277). Fix the signature: fn first_number(text: &str) -> Result<i32, std::num::ParseIntError>, and return Ok(n) at the end.
Question #3
Rewrite this using ?:
fn parse_pair(a: &str, b: &str) -> Result<(i32, 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))
}Show solution
fn parse_pair(a: &str, b: &str) -> Result<(i32, i32), std::num::ParseIntError> {
let x = a.parse::<i32>()?;
let y = b.parse::<i32>()?;
Ok((x, y))
}
Each match becomes a ?. The behavior is identical: any parse failure returns the error early, and the success path builds the tuple.
You now have the full mechanism: panic for bugs, Result for recoverable failure, ? to propagate it cleanly. The next lesson spends it on a problem you've had an IOU on since chapter 7: turning the guessing game's rude expect crash into a polite "try again" loop.