16. The ? Operator and Error Propagation
The `?` operator after a Result or Option expression means "return on failure immediately," turning nested match into a single line. Combined with the `From` trait it auto-converts error types as it propagates up the call stack.
What you'll learn
- 1Propagate Result/Option failures with `?`
- 2Make main return Result so `?` works there
- 3Wire automatic error conversion via the From trait
- 4Know when `?` works on Option too
- 5Use Box<dyn Error> for quick error aggregation
Overview
Match-on-every-step turns into a pyramid of indentation. The `?` character flattens that β "if this step fails, just return upward as is."
Core Concepts
1) ? on a Result
`expr?` is equivalent to:
// expr?
// is shorthand for:
match expr {
Ok(v) => v,
Err(e) => return Err(e.into()), // From-converted
}2) ? on an Option
If the enclosing function returns Option, `?` returns None immediately on None.
3) Auto-conversion via From
If your error type `MyError` implements `From<io::Error>`, then `?` on an io operation auto-converts to `MyError`. This is how you unify several error types into one.
4) main can return Result too
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
let n = "42".parse::<i32>()?;
println!("{}", n);
Ok(())
}Hands-on Examples
Open file and parse first line β without and with `?`:
use std::fs::File;
use std::io::{self, Read};
// without ? β pyramid of indentation
fn read_first_number_v1(path: &str) -> Result<i32, String> {
let mut f = match File::open(path) {
Ok(f) => f,
Err(e) => return Err(format!("open: {}", e)),
};
let mut s = String::new();
if let Err(e) = f.read_to_string(&mut s) {
return Err(format!("read: {}", e));
}
let first = s.lines().next().ok_or("empty")?;
first.trim().parse::<i32>().map_err(|e| format!("parse: {}", e))
}// with ? and Box<dyn Error> β line by line
use std::error::Error;
fn read_first_number_v2(path: &str) -> Result<i32, Box<dyn Error>> {
let mut s = String::new();
File::open(path)?.read_to_string(&mut s)?;
let first = s.lines().next().ok_or("empty")?;
let n = first.trim().parse::<i32>()?;
Ok(n)
}With Option:
fn first_char_uppercased(s: &str) -> Option<char> {
let c = s.chars().next()?; // None β return None
c.to_uppercase().next()
}
fn main() {
println!("{:?}", first_char_uppercased("hello")); // Some('H')
println!("{:?}", first_char_uppercased("")); // None
}Common Mistakes
Q. Does `?` only work inside a function?
A. Yes, and that function must return Result or Option. main can if you give it `Result<(), Box<dyn Error>>`. Outside of fallible contexts `?` is a compile error.
Q. The error types differ β does `?` still work?
A. Yes, as long as `From` is implemented between them. `Box<dyn Error>` is the catch-all type that accepts almost any error (handy for binaries; libraries should prefer concrete error types like `thiserror`).
Q. Can I use `?` in the middle of an expression?
A. Yes: `let n = parse()? + 1;`. Just needs to be in a place where the operand has type Result or Option.
Recap
- `?` = single-character "return on failure"
- Function must return Result or Option to use it
- From-impl provides automatic error conversion
- main returning Result lets you use `?` at the top level
Try It Yourself
- Two-step parse (string β int β positive check) using `?`
- Write `fn first_two_uppercased(s: &str) -> Option<(char, char)>` with `?`
- Refactor main to `-> Result<(), Box<dyn Error>>` and propagate file IO + parsing errors with `?`
All lecture materials and example code are openly available on GitHub.
View on GitHub β