TL;DR: The
?operator is shorthand for: "if this isOk(v), give mev; if it'sErr(e), convertewithFrom::fromand return it immediately." It works onResult<T, E>andOption<T>. It replaces verbosematchblocks and is the standard way to propagate errors in Rust. Functions using?must returnResultorOption. The conversion viaFrommeans?can change error types automatically, the foundation of howthiserrorandanyhowwork.
What Does ? Do?
? unwraps a Result or Option on success, and returns the error/None immediately on failure, propagating it to the caller.
use std::fs;
use std::io;
// Without ? ; verbose
fn read_file_verbose(path: &str) -> Result<String, io::Error> {
match fs::read_to_string(path) {
Ok(content) => match content.parse::<String>() {
Ok(s) => Ok(s),
Err(e) => return Err(e.into()),
},
Err(e) => return Err(e),
}
}
// With ?; same logic, much cleaner
fn read_file(path: &str) -> Result<String, io::Error> {
let content = fs::read_to_string(path)?; // returns early on Err
Ok(content)
}How Does ? Desugar?
expr? expands to a match that calls From::from on the error before returning, this is what enables automatic error type conversion.
// This:
let x = some_result?;
// Expands roughly to:
let x = match some_result {
Ok(val) => val,
Err(err) => return Err(From::from(err)),
};The From::from(err) call is the key, it converts the error to the function's return error type. If conversion isn't possible (no From impl), the compiler errors.
How Do You Chain Multiple Fallible Operations?
Use ? on each step, the function returns on the first failure and each error is automatically converted.
use anyhow::Result;
async fn create_user(pool: &sqlx::PgPool, name: &str, email: &str) -> Result<i64> {
validate_email(email)?; // returns early if invalid
let id = sqlx::query_scalar!(
"INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id",
name, email
)
.fetch_one(pool)
.await?; // returns early if DB error
send_welcome_email(email).await?; // returns early if email fails
Ok(id)
}Without ?, each of these would require a match or .unwrap_or_else block.
Does ? Work on Option?
Yes, ? on Option<T> returns None early if the value is None, or unwraps the Some(v) value.
fn first_even(numbers: &[i32]) -> Option<i32> {
let first = numbers.first()?; // None if slice is empty
if first % 2 == 0 { Some(*first) } else { None }
}
// Converting Option to Result with ?
fn parse_header(headers: &std::collections::HashMap<String, String>) -> Result<String, &'static str> {
let value = headers.get("Authorization").ok_or("missing Authorization header")?;
Ok(value.clone())
}.ok_or(err) converts Option to Result so ? can propagate it as an error.
What Is the Difference Between ?, unwrap(), and expect()?
? propagates errors to the caller. unwrap() and expect() panic on error, use them only in tests or when failure is truly impossible.
? | .unwrap() | .expect("msg") | |
|---|---|---|---|
On Ok(v) / Some(v) | Returns v | Returns v | Returns v |
On Err(e) / None | Returns error to caller | Panics | Panics with message |
| Use in production | ✅ Yes | ❌ Avoid | ❌ Avoid |
| Use in tests | ✅ Yes | ✅ Acceptable | ✅ Acceptable |
| Requires return type | ✅ Result/Option | No | No |
Never use .unwrap() in library code or request handlers, a panic crashes the entire thread or process.
Frequently Asked Questions
? requires the enclosing function to return Result<_, E> or Option<_> because it needs to return Err(...) or return None. Fix it by changing the function's return type, or use .unwrap() / match if you can't change the signature (e.g., fn main()). Note: fn main() -> Result<(), Box<dyn Error>> enables ? in main.
Yes, change fn main() to fn main() -> Result<(), Box<dyn std::error::Error>> or fn main() -> anyhow::Result<()>.
Rust calls From::from(e) to convert. If no From impl exists between the two error types, you get a compile error. Add a From impl, use .map_err(|e| ...), or use anyhow to erase the types.
Only if the closure returns Result or Option. Many iterator adapters don't, use explicit match or collect into Result<Vec<_>, _> to handle errors in iterators.
Sources
Related Glossary Terms
- Result: The type
?primarily operates on - Error Handling: Rust's broader error handling model
- From / Into: The conversion
?uses under the hood - anyhow: Makes
?work across any error type - thiserror: Generates
Fromimpls that power?
Keep Reading
- Rust Error Handling: Result and Option: the ? operator is the ergonomic face of Result propagation
- Rust for Python Developers: Python try/except vs Rust's ? operator
- Learn Rust in 2026: ? is one of the first pieces of syntax that clicks
