The ? operator is syntactic sugar for propagating errors from a function that returns a Result or Option to the caller, automatically returning early if an error or None is encountered. It saves you from writing verbose match statements or if let blocks for every single fallible operation in your function.
When you use ? on a Result<T, E>, it checks the value: if it's Ok, it unwraps and returns the inner T; if it's Err, it immediately returns that error from the current function. For this to work, the function using ? must return a Result (or Option) with an error type that is compatible with the error being propagated. Rust's From trait handles the conversion if the error types don't match exactly.
Here is a practical example showing how ? simplifies error handling compared to manual matching:
use std::fs::File;
use std::io::{self, Read};
// Without '?', you need nested matches or if-lets
fn read_file_manual(path: &str) -> Result<String, io::Error> {
let mut file = match File::open(path) {
Ok(f) => f,
Err(e) => return Err(e),
};
let mut contents = String::new();
match file.read_to_string(&mut contents) {
Ok(_) => Ok(contents),
Err(e) => Err(e),
}
}
// With '?', the logic is linear and concise
fn read_file_clean(path: &str) -> Result<String, io::Error> {
let mut file = File::open(path)?; // Returns Err immediately if open fails
let mut contents = String::new();
file.read_to_string(&mut contents)?; // Returns Err immediately if read fails
Ok(contents)
}
You can also use ? with Option<T>. If the value is Some, it extracts the inner value; if it is None, the function returns None immediately. This requires the function to return an Option.
fn get_user_age(user_id: u32) -> Option<u32> {
let user = find_user(user_id)?; // Returns None if find_user returns None
let age = user.age()?; // Returns None if age() returns None
Some(age)
}
A critical constraint is that the ? operator can only be used inside functions that return Result, Option, or a type implementing FromResidual (like impl Future in async contexts). You cannot use it in main unless main returns a Result (e.g., fn main() -> Result<(), Box<dyn std::error::Error>>). If you try to use ? in a function returning (), the compiler will error because there is no error type to propagate to.