The ? operator
Intermediate · Abstractions
What & why
Writing a match at every fallible step — parse this, or return the error; read that, or return the error — gets tedious fast, and the boilerplate drowns out the actual logic. The ? operator is the shortcut: put it after something that returns Result (or Option), and it means “if this succeeded, give me the value; if it failed, stop and return that failure from my function right now.” It only works inside a function whose own return type is Result/Option, and — its real superpower — it can convert between different error types along the way.
The idea, slowly
? is sugar for a match
Take this function without ?:
use std::num::ParseIntError;
fn double_from_text_verbose(text: &str) -> Result<i32, ParseIntError> {
let n = match text.parse::<i32>() {
Ok(value) => value,
Err(e) => return Err(e), // bail out immediately with the error
};
Ok(n * 2)
}
fn main() {
println!("{:?}", double_from_text_verbose("10")); // Ok(20)
println!("{:?}", double_from_text_verbose("nope")); // Err(ParseIntError { .. })
}
? collapses that entire match into one character:
use std::num::ParseIntError;
fn double_from_text(text: &str) -> Result<i32, ParseIntError> {
let n = text.parse::<i32>()?; // on error, return the Err right here
Ok(n * 2) // on success, continue
}
fn main() {
println!("{:?}", double_from_text("10")); // Ok(20)
println!("{:?}", double_from_text("nope")); // Err(ParseIntError { .. })
}
What the compiler is thinking: at the ?, it inserts “check: is this Err? If so, return Err(...) right now — converting the error type if needed (more on that below). Otherwise, unwrap the Ok and keep going.” Notice the success path still wraps the answer in Ok(...) — ? only handles the early-return side; the function’s normal return still needs to produce a Result.
Why ? only works in a Result/Option-returning function
?’s early return has to return something from the enclosing function — specifically, an Err (or None). If the function doesn’t return Result/Option, there’s nowhere for that early return to go, and the compiler refuses:
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option`
The fix is always the same shape: give the function a Result<T, E> (or Option<T>) return type so ? has something to return early with.
The hidden superpower: ? converts error types via From
Real functions often call into several things that fail with different error types. ? doesn’t just return the error as-is — it calls From::from on it, converting it into whatever error type the function declares. As long as From<SourceError> for MyError exists, ? uses it automatically:
use std::error::Error;
fn parse_env_number(key: &str) -> Result<i32, Box<dyn Error>> {
let text = std::env::var(key)?; // VarError converts into Box<dyn Error>
let n: i32 = text.parse()?; // ParseIntError converts into Box<dyn Error>
Ok(n)
}
fn main() {
match parse_env_number("PORT") {
Ok(n) => println!("port: {n}"),
Err(e) => println!("couldn't read PORT: {e}"),
}
}
Here std::env::var fails with VarError and .parse() fails with ParseIntError — two unrelated types — but both ?s work because Box<dyn Error> has a blanket From impl for any type implementing std::error::Error. The function only has to declare one error type; ? does the conversion at each call site. (The next two lessons build on exactly this: writing your own error type with From impls, and letting thiserror/anyhow generate them for you.)
? works on Option too
The same operator works in a function returning Option: on Some, it unwraps; on None, it returns None immediately.
fn first_upper_char(text: &str) -> Option<char> {
let c = text.chars().next()?; // None if text is empty — return None right here
Some(c.to_ascii_uppercase())
}
fn main() {
println!("{:?}", first_upper_char("rust")); // Some('R')
println!("{:?}", first_upper_char("")); // None
}
main can return a Result too
Because ? needs a Result/Option-returning function to work in, and you’ll often want to use ? at the top level, fn main is allowed to return Result<(), E>:
fn main() -> Result<(), std::num::ParseIntError> {
let n: i32 = "123".parse()?;
println!("got {}", n);
Ok(())
}
Ok(()) means “succeeded, with no meaningful value” — () is Rust’s empty type. If a ? inside main hits an error, the program exits with a nonzero status and prints the error using its Debug output.
Common mistakes
- Using
?in a function that doesn’t returnResult/Option. The error isthe ? operator can only be used in a function that returns Result or Option. Change the function’s return type, or handle the error withmatchinstead. - Forgetting to wrap the success value in
Ok(...). In a-> Result<...>function, the happy path must returnOk(value), not a barevalue.?only rewrites the error path; the normal return is still your job. - Using
?across two error types with noFromimpl between them. If your function returnsResult<T, ParseIntError>but you?on something that fails withstd::io::Error, the compiler can’t find a conversion and refuses to build. Either widen the return type (e.g. toBox<dyn Error>), or write theFromimpl yourself (next lesson). - Expecting
?to work in a closure the same way it does in the enclosing function.?returns from the nearest enclosing function — inside a closure, that’s the closure, notmain. If the closure’s return type isn’tResult/Optiontoo, it won’t compile.
More examples
Chain three fallible steps in one function
Real functions rarely stop at one ? — computing an order total might mean parsing a quantity, a price, and a tax figure, each of which can fail on its own.
use std::num::ParseIntError;
fn total_cost(qty_text: &str, price_text: &str, tax_text: &str) -> Result<i32, ParseIntError> {
let qty: i32 = qty_text.parse()?; // step 1
let price: i32 = price_text.parse()?; // step 2
let tax: i32 = tax_text.parse()?; // step 3
Ok(qty * price + tax)
}
fn main() {
println!("{:?}", total_cost("3", "20", "5")); // Ok(65)
println!("{:?}", total_cost("3", "oops", "5")); // Err(...)
}
Convert a library error into your own error type
Instead of erasing everything into Box<dyn Error>, a small app-specific error enum with a From impl lets ? convert automatically while keeping a concrete, matchable type.
use std::num::ParseIntError;
#[derive(Debug)]
enum ConfigError {
BadNumber(ParseIntError),
}
impl From<ParseIntError> for ConfigError {
fn from(e: ParseIntError) -> Self {
ConfigError::BadNumber(e)
}
}
fn read_port(text: &str) -> Result<i32, ConfigError> {
let port: i32 = text.parse()?; // ParseIntError auto-converts via From
Ok(port)
}
fn main() {
println!("{:?}", read_port("8080")); // Ok(8080)
println!("{:?}", read_port("nope")); // Err(BadNumber(...))
}
? inside a helper, called from main
A helper function’s Result doesn’t stop at its own boundary — call it with ? from another Result-returning function, including main itself.
fn parse_pair(a: &str, b: &str) -> Result<(i32, i32), std::num::ParseIntError> {
Ok((a.parse()?, b.parse()?))
}
fn main() -> Result<(), std::num::ParseIntError> {
let (x, y) = parse_pair("4", "5")?; // helper's Result propagates into main
println!("sum: {}", x + y);
Ok(())
}
? on an Option, inside a function returning Option
Extracting a filename’s extension is a classic “might not exist” lookup — ? on Option bails out cleanly the moment there’s nothing to find.
fn file_extension(name: &str) -> Option<&str> {
let dot_index = name.rfind('.')?; // None if there's no dot at all
Some(&name[dot_index + 1..])
}
fn main() {
println!("{:?}", file_extension("report.pdf")); // Some("pdf")
println!("{:?}", file_extension("README")); // None
}
Stop a batch job at the first bad value
Inside a loop, ? still bails out of the whole function on the first failure — handy for validating a batch of input where one bad value should stop everything.
fn parse_all(values: &[&str]) -> Result<Vec<i32>, std::num::ParseIntError> {
let mut out = Vec::new();
for v in values {
out.push(v.parse::<i32>()?); // bails out of the whole function on the first bad value
}
Ok(out)
}
fn main() {
println!("{:?}", parse_all(&["1", "2", "3"])); // Ok([1, 2, 3])
println!("{:?}", parse_all(&["1", "oops", "3"])); // Err(...)
}
Your turn
This function should read a PORT environment variable and parse it as a number, but it doesn’t compile.
use std::num::ParseIntError;
fn parse_env_number(key: &str) -> Result<i32, ParseIntError> {
let text = std::env::var(key)?; // env::var fails with VarError, not ParseIntError
let n: i32 = text.parse()?;
Ok(n)
}
fn main() {
println!("{:?}", parse_env_number("PORT"));
}
Show solution
std::env::var fails with std::env::VarError, but the function’s declared error type is ParseIntError. ? tries to convert the error via From::from, but there’s no From<VarError> for ParseIntError — so the compiler rejects it with a type mismatch on the ?.
The simplest fix is to widen the return type to something both error types can convert into, like Box<dyn std::error::Error>:
use std::error::Error;
fn parse_env_number(key: &str) -> Result<i32, Box<dyn Error>> {
let text = std::env::var(key)?; // VarError -> Box<dyn Error>
let n: i32 = text.parse()?; // ParseIntError -> Box<dyn Error>
Ok(n)
}
fn main() {
println!("{:?}", parse_env_number("PORT"));
}
Both VarError and ParseIntError implement std::error::Error, and there’s a blanket From impl that converts any such type into Box<dyn Error>, so both ?s now compile. (The next lesson shows the alternative: a custom error enum with explicit From impls, which keeps the concrete error type instead of erasing it into a trait object.)
Quick check
Remember this
expr?means: on success, give me the inner value; on failure, return early from this function with the error.?only compiles inside a function that itself returnsResultorOption— there’s nowhere else for the early return to go.?callsFrom::fromon the error, so a function can return one error type while?-ing through several different underlying error types — as long as aFromconversion exists (or the target isBox<dyn Error>, which accepts anything).- The happy path still needs an explicit
Ok(value)(orSome(value)) —?only handles the early-return side. fn main() -> Result<(), E>lets you use?directly inmain.
Go deeper
- Rust Book - Propagating Errors — Where the ? operator is introduced.
Next: