Error Propagation
The ? operator:
- Works with
ResultandOption. - If the value is success (
Ok/Some), it unwraps it. - If the value is failure (
Err/None), it returns early from the current function with that error.
In short:
?= “Try this. If it fails, return the error immediately.”
Why use ??
Without ?, error handling gets verbose:
fn read_number(path: &str) -> Result<i32, std::io::Error> {
let contents = match std::fs::read_to_string(path) {
Ok(c) => c,
Err(e) => return Err(e),
};
let number = match contents.trim().parse::<i32>() {
Ok(n) => n,
Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e)),
};
Ok(number)
}
```rust
With `?`, it becomes much cleaner:
```rust
fn read_number(path: &str) -> Result<i32, std::io::Error> {
let contents = std::fs::read_to_string(path)?;
let number = contents.trim().parse::<i32>()
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
Ok(number)
}
How ? works with Result
Example 1: File reading
use std::fs::File;
use std::io::{self, Read};
fn read_file(path: &str) -> Result<String, io::Error> {
let mut file = File::open(path)?; // If Err, return Err
let mut contents = String::new();
file.read_to_string(&mut contents)?; // If Err, return Err
Ok(contents)
}
fn main() {
match read_file("hello.txt") {
Ok(text) => println!("File contents:\n{}", text),
Err(e) => println!("Error reading file: {}", e),
}
}
- If
File::openfails → function returns immediately with that error. - If
read_to_stringfails → same. - Otherwise, returns
Ok(contents).
How ? works with Option
Example 2: Chaining optional values
fn first_even_double(nums: &[i32]) -> Option<i32> {
let first = nums.first()?; // If empty, return None
let even = if first % 2 == 0 {
Some(*first)
} else {
None
}?;
Some(even * 2)
}
fn main() {
let nums = vec![2, 4, 6];
println!("{:?}", first_even_double(&nums)); // Some(4)
let empty: Vec<i32> = vec![];
println!("{:?}", first_even_double(&empty)); // None
}
If any step returns None, the whole function returns None.
Rules for using ?
- The function must return a compatible type:
- Use
?onResult<T, E>→ function must returnResult<_, E>(or compatible error). - Use
?onOption<T>→ function must returnOption<_>.
- Use
- Error types must match or be convertible.
Automatic error conversion with From
If your function returns a different error type, Rust uses From to convert automatically.
Example 3: Custom error type
use std::fs;
use std::num::ParseIntError;
use std::io;
#[derive(Debug)]
enum MyError {
Io(io::Error),
Parse(ParseIntError),
}
impl From<io::Error> for MyError {
fn from(err: io::Error) -> MyError {
MyError::Io(err)
}
}
impl From<ParseIntError> for MyError {
fn from(err: ParseIntError) -> MyError {
MyError::Parse(err)
}
}
fn read_number(path: &str) -> Result<i32, MyError> {
let contents = fs::read_to_string(path)?; // io::Error → MyError
let number = contents.trim().parse::<i32>()?; // ParseIntError → MyError
Ok(number)
}
?automatically converts errors usingFrom.
Example 4: Using ? in main
Rust allows main to return Result:
fn main() -> Result<(), Box<dyn std::error::Error>> {
let text = std::fs::read_to_string("hello.txt")?;
println!("{}", text);
Ok(())
}
- Any error will automatically be printed and the program exits with a failure code.
? vs unwrap() / expect()
| Feature | ? | unwrap() / expect() |
|---|---|---|
| On failure | Returns error to caller | Panics |
| Use case | Recoverable errors | Bugs / impossible cases |
| Code safety | Safer, composable | Risky in production |
Summary
?propagates errors automatically.- Works with
ResultandOption. - Eliminates boilerplate
matchandif let. - Uses
Fromfor error type conversion. - Encourages writing clean, safe, composable Rust code.