The ? Operator
The ? operator is Rust’s shorthand for propagating a fallible operation’s failure straight up to the caller. Instead of writing a match at every step of a call chain just to check whether something succeeded, you write a single question mark after the expression, and Rust handles the check for you. It works with both Result<T, E> and Option<T>, and it is the idiomatic way error handling is written throughout real-world Rust code.
Overview: How the ? Operator Works
Before you can appreciate ?, it helps to see the problem it solves. Any function that can fail in Rust returns Result<T, E> — either Ok(value) on success or Err(error) on failure — or, for a value that might simply be absent rather than wrong, Option<T> (Some(value) or None). If function a calls fallible function b, and a itself wants to fail whenever b fails, the manual way to write that is a match: check the result, pull out the value on success, and immediately return the error unchanged on failure. That pattern repeats at every fallible call in a program, and typing it out each time buries the real logic under a wall of boilerplate.
The ? operator collapses that entire match into one character placed right after the expression. Picture a fork placed at every fallible call inside your function: as long as results keep coming back Ok (or Some), execution drives straight through, and ? simply hands you the unwrapped value to keep working with. The moment one call comes back Err (or None), traffic takes an immediate exit ramp — the enclosing function stops right there and returns that failure to whoever called it. Nothing after the ? on that line, or anywhere later in the function, ever runs.
This is not exception handling in the C++/Java sense — there is no stack-unwinding machinery, no hidden control flow jumping across arbitrary call frames. It is a plain, typed, early return that the compiler writes for you. Because the failure path is just an ordinary return value, the function signature always tells the full story of what can go wrong, and the compiler checks every case is handled. You can trace a ? by eye — it always means "return early from this function" and nothing more exotic.
Crucially, ? only works inside a function whose own return type is compatible with what you are applying it to: use it on a Result<T, E> only inside a function that itself returns Result<_, F> (where E can convert into F), and use it on an Option<T> only inside a function returning Option<_>. The compiler enforces this at the type level, so a mismatch is caught before your program ever runs — you will see exactly this in Common Mistakes below.
Syntax
The general form is an expression immediately followed by ?:
// General form
let value = fallible_call()?;
// Roughly equivalent to (simplified):
let value = match fallible_call() {
Ok(v) => v,
Err(e) => return Err(From::from(e)),
};
The table below summarizes what ? does depending on what it is applied to:
| Applied to | On success | On failure | Enclosing function must return |
|---|---|---|---|
Result<T, E> |
Evaluates to the inner T |
Returns Err(From::from(e)) immediately |
Result<_, F> where E: Into<F> |
Option<T> |
Evaluates to the inner T |
Returns None immediately |
Option<_> |
- The expression before
?must produce aResultor anOption— anything else is a compile error. - The conversion on the error path calls
From::from, which is why a function can use?on several different concrete error types as long as each one implementsFrominto the function’s declared error type (more on this below). - You cannot mix a
Result-producing expression with?inside a function that returnsOption, or vice versa, without first converting one into the other (for example with.ok()or.ok_or(...)).
Examples
Example 1: Propagating a Parse Error
Parsing text into a number can fail, so str::parse returns a Result. This function parses two strings and adds them, propagating either parse failure with ?:
use std::num::ParseIntError;
fn parse_and_add(a: &str, b: &str) -> Result<i32, ParseIntError> {
let x = a.parse::<i32>()?;
let y = b.parse::<i32>()?;
Ok(x + y)
}
fn main() {
match parse_and_add("10", "32") {
Ok(sum) => println!("Sum: {}", sum),
Err(e) => println!("Error: {}", e),
}
match parse_and_add("10", "abc") {
Ok(sum) => println!("Sum: {}", sum),
Err(e) => println!("Error: {}", e),
}
}
Output:
Sum: 42
Error: invalid digit found in string
In the first call both "10" and "32" parse successfully, so both ?s just unwrap their values and parse_and_add returns Ok(42). In the second call, "10" still parses fine, but "abc" does not — the second ? sees an Err, and parse_and_add returns immediately with that error. The line Ok(x + y) never executes for that call.
Example 2: Using ? with Option
? works the same way on Option<T>, treating None as the failure case:
fn first_char_of_first_word(text: &str) -> Option<char> {
let first_word = text.split_whitespace().next()?;
first_word.chars().next()
}
fn main() {
println!("{:?}", first_char_of_first_word("hello world"));
println!("{:?}", first_char_of_first_word(" "));
}
Output:
Some('h')
None
split_whitespace().next() returns Option<&str>. For "hello world" that is Some("hello"), so ? unwraps it and the function goes on to return the first character. For " " (only whitespace), split_whitespace yields nothing, next() returns None, and ? immediately returns None from first_char_of_first_word without ever reaching the second line.
Example 3: Using ? Directly in main
Since Rust 1.26, fn main() is allowed to return Result<(), E> (for any E that implements Debug), which means you can use ? right inside main instead of unwrapping everything:
use std::num::ParseIntError;
fn double(input: &str) -> Result<i32, ParseIntError> {
let n = input.parse::<i32>()?;
Ok(n * 2)
}
fn main() -> Result<(), ParseIntError> {
let result = double("21")?;
println!("Doubled: {}", result);
Ok(())
}
Output:
Doubled: 42
double("21") succeeds, so ? unwraps it to 42 and execution continues to the println!. Had double returned an Err instead, the ? in main would have returned that Err straight out of main itself — the process would exit with a non-zero status and print the error’s Debug representation to stderr, with no println! ever running. This pattern is convenient for small programs and examples where you would otherwise be tempted to sprinkle .unwrap() everywhere.
Propagating Different Error Types
A Custom Error Enum with From
Real functions often have more than one way to fail, and those failures often come from different underlying types. A function that both parses a number and validates it needs one error type that can represent both a parse failure and a validation failure. Defining an enum and implementing From for the parts you want ? to auto-convert is the idiomatic solution:
use std::fmt;
use std::num::ParseIntError;
#[derive(Debug)]
enum AppError {
Parse(ParseIntError),
OutOfRange(i32),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::Parse(e) => write!(f, "could not parse number: {}", e),
AppError::OutOfRange(n) => write!(f, "{} is out of the allowed range 0..=100", n),
}
}
}
impl From<ParseIntError> for AppError {
fn from(e: ParseIntError) -> Self {
AppError::Parse(e)
}
}
fn parse_percentage(input: &str) -> Result<i32, AppError> {
let n = input.parse::<i32>()?;
if n < 0 || n > 100 {
return Err(AppError::OutOfRange(n));
}
Ok(n)
}
fn main() {
for input in ["75", "150", "notanumber"] {
match parse_percentage(input) {
Ok(n) => println!("{} -> valid: {}%", input, n),
Err(e) => println!("{} -> error: {}", input, e),
}
}
}
Output:
75 -> valid: 75%
150 -> error: 150 is out of the allowed range 0..=100
notanumber -> error: could not parse number: invalid digit found in string
Because AppError implements From<ParseIntError>, the ? on input.parse::<i32>() is allowed even though that expression produces a ParseIntError, not an AppError — the compiler inserts the conversion automatically. The range check is a separate failure path that builds an AppError::OutOfRange directly with a plain return Err(...), since there is nothing to convert there.
Box<dyn Error> for Quick Unification
Writing a dedicated enum for every combination of error sources is the most precise approach, but it is not always worth the ceremony, especially in application code (as opposed to a library other people depend on). Box<dyn std::error::Error> is a trait object that can hold any type implementing the standard Error trait, and the standard library provides a blanket From implementation for it — so ? can convert practically any std error type into it without you writing a single From impl:
use std::error::Error;
use std::fs;
fn read_and_parse(path: &str) -> Result<i32, Box<dyn Error>> {
let contents = fs::read_to_string(path)?;
let n = contents.trim().parse::<i32>()?;
Ok(n)
}
fn main() {
match read_and_parse("number.txt") {
Ok(n) => println!("Parsed: {}", n),
Err(e) => println!("Failed: {}", e),
}
}
Output:
Failed: No such file or directory (os error 2)
The first ? can fail with a std::io::Error (the file does not exist here) and the second could fail with a ParseIntError; both implement Error, so both convert into Box<dyn Error> automatically. If you create a number.txt containing a valid integer, the same program prints Parsed: <that number> instead. The trade-off is that callers of read_and_parse can only display or log the boxed error — they cannot match on which specific variant occurred without downcasting. For larger applications, crates like anyhow (for quick, ergonomic error unification) and thiserror (for deriving precise custom error enums in libraries) build on exactly this idea.
How It Works Step by Step
Take the failing call from Example 1, parse_and_add("10", "abc"), and trace it:
a.parse::<i32>()runs on"10"and producesOk(10). The?seesOk, so it evaluates to10, which is bound tox. Execution continues normally.b.parse::<i32>()runs on"abc"and producesErr(ParseIntError { .. }). The?seesErr, callsFrom::fromon the error (here it is already the right type, so the conversion is the identity), and immediately performsreturn Err(...)fromparse_and_add.- Because that
returnhappens insideparse_and_add, the lineOk(x + y)is never reached —yis never even bound. - Control returns to the
matchexpression inmain, which binds the propagated value asErr(e)and prints it.
This is exactly what the desugaring in the Syntax section describes: ? is a compiler-inserted match that unwraps the success case and performs an early, converted return on the failure case. Because the conversion step always goes through From::from, implementing From<SomeError> for YourError is precisely what makes ? able to bridge two different concrete error types, as shown with AppError above.
Common Mistakes
Mistake 1: Using ? Outside a Result/Option-Returning Function
? needs somewhere to send the failure — the enclosing function’s return type has to match. This does not compile:
fn parse_number(s: &str) -> i32 {
let n = s.parse::<i32>()?;
n
}
The compiler rejects it with an error along the lines of "the ? operator can only be used in a function that returns Result or Option (or another type that implements FromResidual)", because parse_number returns a bare i32, which has no way to represent a failure. The fix is to change the return type to something that can carry an error, and let the caller decide what to do:
fn parse_number(s: &str) -> Result<i32, std::num::ParseIntError> {
let n = s.parse::<i32>()?;
Ok(n)
}
Mistake 2: Propagating an Error Type That Doesn’t Match
? converts the error via From, but only if a matching From implementation actually exists. This looks reasonable but fails to compile:
use std::fs;
use std::num::ParseIntError;
fn read_and_parse(path: &str) -> Result<i32, ParseIntError> {
let contents = fs::read_to_string(path)?;
let n = contents.trim().parse::<i32>()?;
Ok(n)
}
fs::read_to_string returns Result<String, std::io::Error>, but the function’s declared error type is ParseIntError. Since ParseIntError does not implement From<std::io::Error>, the compiler rejects the first ? with an error stating that the trait From<std::io::Error> is not implemented for ParseIntError. The fix is to widen the function’s error type to something both underlying errors can convert into, such as Box<dyn Error> or a custom enum with From impls for both:
use std::error::Error;
use std::fs;
fn read_and_parse(path: &str) -> Result<i32, Box<dyn Error>> {
let contents = fs::read_to_string(path)?;
let n = contents.trim().parse::<i32>()?;
Ok(n)
}
Best Practices
- Prefer
?over chains of.unwrap()outside of quick teaching snippets or genuinely infallible cases;.unwrap()turns a recoverable error into an instant panic. - Give library functions precise, specific error types (an enum implementing
std::error::Error) so callers canmatchon the failure and decide how to respond; reserveBox<dyn Error>for application code that mainly needs to display or log the error. - Implement
Fromfor every error type you want a function to accept through?, rather than manually matching and converting at each call site — automatic conversion is the entire point of?. - Let
fn main() -> Result<(), E>handle the top-level?in small binaries and examples instead of unwrapping every fallible call by hand. - Remember
?only propagates — it does not add context. If a caller needs to know which file or step failed, wrap the error with extra information (a custom variant carrying the path, for example) before returning it. - Do not reach for
?just to avoid thinking about theNone/Errcase; sometimes handling it locally withmatchor a combinator like.unwrap_or_else()is the more correct, more localized choice.
Practice Exercises
- Write
fn parse_two(a: &str, b: &str) -> Result<(i32, i32), std::num::ParseIntError>that parses both strings with?and returns them as a tuple. Call it once with two valid numbers and once with an invalid one, printing the result each time. - Write
fn first_and_last(s: &str) -> Option<(char, char)>that uses?on.chars().next()to get the first character and returnsNonefor an empty string, then also returns the last character with.chars().last(). Calling it with"rust"should printSome(('r', 't')). - Define your own error enum with two variants wrapping
std::num::ParseIntErrorandstd::io::Error, implementFromfor each, and write a function that reads a file’s contents and parses the trimmed text as ani32, propagating both kinds of failure through?into your enum. Print theDisplaymessage you get back when the file does not exist.
Summary
?unwrapsOk/Someand returnsErr/Noneearly from the enclosing function, replacing a manualmatchat every fallible call.- It only compiles inside a function whose own return type is
ResultorOption, matching what is on the left of the?. - On the failure path it calls
From::from, so a function can accept several different concrete error types through?as long as each implementsFrominto its declared error type. Box<dyn Error>unifies many error types quickly without a custom enum, at the cost of losing the ability to match on a specific variant.?is not exception handling — there is no stack unwinding; it is sugar for an explicit, typed, early return.- Use
?to keep fallible code readable, and reserve.unwrap()for cases where failure truly cannot happen or a panic is genuinely the intended behavior.
