Skip to content

The ? Operator

Without the ? operator, propagating an error from a called function requires a match expression every time:

fn parse_and_double(s: &str) -> Result<i32, std::num::ParseIntError> {
let n = match s.parse::<i32>() {
Ok(v) => v,
Err(e) => return Err(e),
};
Ok(n * 2)
}

This pattern — unwrap the Ok or return the Err early — is so common that Rust has a dedicated operator for it.

Placing ? after a Result expression does exactly the same thing as the match above:

  • If the value is Ok(v), it evaluates to v.
  • If the value is Err(e), it returns Err(e) immediately from the enclosing function.

The function must return Result (or Option) for ? to work — the compiler enforces this.

use std::num::ParseIntError;
fn parse_and_double(s: &str) -> Result<i32, ParseIntError> {
let n: i32 = s.parse()?; // ? returns early on Err
Ok(n * 2)
}

? is not magic — it is sugar for a match that either extracts the value or converts the error and returns it. On a Result<T, E> expression inside a function returning Result<T2, E2>, expr? is exactly:

// @skip-verify illustrative desugaring only — `expr` is a placeholder, not real code
match expr {
Ok(v) => v,
Err(e) => return Err(std::convert::From::from(e)),
}

Both forms below compile to the same thing — they are here side by side so you can see precisely what the sugar hides:

use std::num::ParseIntError;
// Desugared by hand
fn parse_and_double_manual(s: &str) -> Result<i32, ParseIntError> {
let n: i32 = match s.parse::<i32>() {
Ok(v) => v,
Err(e) => return Err(std::convert::From::from(e)),
};
Ok(n * 2)
}
// Using ?
fn parse_and_double_sugar(s: &str) -> Result<i32, ParseIntError> {
let n: i32 = s.parse()?;
Ok(n * 2)
}
fn main() {
println!("{:?}", parse_and_double_manual("21"));
println!("{:?}", parse_and_double_sugar("21"));
}
flowchart TD
  A["expr?"] --> B{"expr is Ok or Err?"}
  B -->|"Ok(v)"| C["Evaluates to v,
execution continues"]
  B -->|"Err(e)"| D["From::from(e) converts
the error type if needed"]
  D --> E["return Err(converted_e)
from the enclosing function"]
What expr? does

Because ? returns the Ok value directly, you can call multiple fallible functions in sequence without nesting:

use std::num::ParseIntError;
fn parse_and_double(s: &str) -> Result<i32, ParseIntError> {
let n: i32 = s.parse()?;
Ok(n * 2)
}
fn chain(a: &str, b: &str) -> Result<i32, ParseIntError> {
let x = parse_and_double(a)?;
let y = parse_and_double(b)?;
Ok(x + y)
}

Each ? either unwraps the success value or short-circuits the whole function. No nested matches, no manual early returns.

What the Compiler Says: ? Needs a Result/Option-Returning Function

Section titled “What the Compiler Says: ? Needs a Result/Option-Returning Function”

The desugaring above only type-checks when the enclosing function can hold onto the early return Err(...) — which means it must itself return Result (or Option). Using ? inside a function returning () is a compile error, not a runtime one:

// @expect-error E0277
fn main() {
let x: Result<i32, String> = Err(String::from("no"));
let n = x?;
println!("{}", n);
}
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
--> question_unit.rs:3:14
|
1 | fn main() {
| --------- this function should return `Result` or `Option` to accept `?`
2 | let x: Result<i32, String> = Err(String::from("no"));
3 | let n = x?;
| ^ cannot use the `?` operator in a function that returns `()`
|
help: consider adding return type
|
1 ~ fn main() -> Result<(), Box<dyn std::error::Error>> {
2 | let x: Result<i32, String> = Err(String::from("no"));
3 | let n = x?;
4 | println!("{}", n);
5 + Ok(())
|
error: aborting due to 1 previous error

The compiler’s own suggestion is exactly the pattern in the next section: give main a Result return type.

main can also return Result<(), Box<dyn std::error::Error>>, which lets you use ? inside main itself:

fn main() -> Result<(), Box<dyn std::error::Error>> {
let n: i32 = "42".parse()?;
println!("parsed: {}", n);
Ok(())
}

Box<dyn std::error::Error> is a trait object that can hold any error type, making it useful in main where you do not want to fix a single concrete error type.

When main returns Err(e), the Rust runtime prints Error: {:?} (the Debug formatting of e) to stderr and exits with status 1 — no panic, no backtrace by default, just that one line:

// @expect-panic exits with status 1, which the harness treats like a non-zero-exit demo
fn main() -> Result<(), Box<dyn std::error::Error>> {
let n: i32 = "abc".parse()?;
println!("parsed: {}", n);
Ok(())
}
$ rustc --edition 2024 main_parse.rs -o main_parse && ./main_parse; echo "exit=$?"
Error: ParseIntError { kind: InvalidDigit }
exit=1

Note what did not happen: println!("parsed: {}", n) never ran (the ? returned early), and there is no thread 'main' panicked at ... line — this is a controlled, non-panicking termination. Contrast this with std::process::exit, covered in the Panics lesson: unlike an explicit process::exit call, returning Err from main is a normal function return, so any value still in scope in main is dropped exactly as it would be on any other return:

// @expect-panic exits with status 1 (Err from main), no panic message involved
use std::error::Error;
use std::fmt;
#[derive(Debug)]
struct Boom;
impl fmt::Display for Boom {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "boom")
}
}
impl Error for Boom {}
struct Guard;
impl Drop for Guard {
fn drop(&mut self) {
println!("dropping guard");
}
}
fn might_fail() -> Result<(), Boom> {
Err(Boom)
}
fn main() -> Result<(), Box<dyn Error>> {
let _g = Guard;
might_fail()?;
Ok(())
}
$ rustc --edition 2024 main_drop.rs -o main_drop && ./main_drop; echo "exit=$?"
dropping guard
Error: Boom
exit=1

dropping guard prints before Error: Boom — the Guard’s destructor ran during the normal unwind-free return out of main, the exact opposite of the std::process::exit/panic = "abort" behaviour where destructors are skipped entirely.

? works the same way inside a function returning Option<T>: Some(v) unwraps to v, None returns None immediately from the enclosing function. There is no error type to convert, so no From call is involved:

fn first_char_upper(s: &str) -> Option<char> {
let c = s.chars().next()?; // returns None early if s is empty
Some(c.to_ascii_uppercase())
}
fn main() {
println!("{:?}", first_char_upper("hello"));
println!("{:?}", first_char_upper(""));
}

? cannot mix the two: you cannot use it on a Result inside a function returning Option, or vice versa, without an explicit conversion first (.ok() for Result -> Option, .ok_or(...) for Option -> Result — both covered in the Result lesson).

When the error type returned by the called function differs from the error type of the enclosing function, ? automatically calls From::from to convert — this is the From::from(e) step in the desugaring shown earlier. This is how a function returning MyError can use ? on a call that returns ParseIntError, as long as From<ParseIntError> for MyError is implemented.

If the target error type has no From implementation for the source error, ? fails to compile — with a message that names the exact trait you are missing:

// @expect-error E0277
use std::num::ParseIntError;
#[derive(Debug)]
struct MyError(String);
fn inner(s: &str) -> Result<i32, ParseIntError> {
s.parse()
}
fn outer(s: &str) -> Result<i32, MyError> {
let n = inner(s)?;
Ok(n)
}
fn main() {
match outer("abc") {
Ok(n) => println!("{}", n),
Err(e) => println!("{:?}", e),
}
}
error[E0277]: `?` couldn't convert the error to `MyError`
--> question_mismatch.rs:11:21
|
10 | fn outer(s: &str) -> Result<i32, MyError> {
| -------------------- expected `MyError` because of this
11 | let n = inner(s)?;
| --------^ the trait `From<ParseIntError>` is not implemented for `MyError`
| |
| this can't be annotated with `?` because it has type `Result<_, ParseIntError>`
|
note: `MyError` needs to implement `From<ParseIntError>`
--> question_mismatch.rs:4:1
|
4 | struct MyError(String);
| ^^^^^^^^^^^^^^
= note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` trait
error: aborting due to 1 previous error

The fix is exactly what the note says — implement the missing conversion once, and every ? call site benefits automatically:

use std::num::ParseIntError;
fn parse_and_double(s: &str) -> Result<i32, ParseIntError> {
let n: i32 = s.parse()?; // ? returns early on Err
Ok(n * 2)
}
fn chain(a: &str, b: &str) -> Result<i32, ParseIntError> {
let x = parse_and_double(a)?;
let y = parse_and_double(b)?;
Ok(x + y)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let doubled = parse_and_double("21")?;
println!("doubled: {}", doubled);
let sum = chain("10", "5")?;
println!("chain(10, 5): {}", sum);
// Demonstrate early return on error
match parse_and_double("abc") {
Ok(v) => println!("ok: {}", v),
Err(e) => println!("early return caught: {}", e),
}
println!("main returned Ok(())");
Ok(())
}

Read expr? as “assume this succeeded, and if it did not, this whole function is over — hand the failure to whoever called me, converted into my own error currency along the way.” The From::from step is the important, easy-to-miss part: it means the caller’s error type is the one that matters at every ? site, not the callee’s, and a single From impl per error-type pair is enough to make every future ? call between those two types just work.

? makes the happy path of a function read top-to-bottom with no nesting, which is a large readability win once a function calls more than one or two fallible operations. Its cost is that the actual control flow — an early return — is compressed into a single punctuation character, which is easy to skim past when reading unfamiliar code, and it requires every intermediate error type in a call chain to either already be From-convertible into the function’s declared error type or be erased into Box<dyn Error>. Reaching for Box<dyn Error> to sidestep writing From impls trades that one-time cost for a real, ongoing one: callers of your function lose the ability to match on the specific failure (covered in the Custom Errors lesson).

  • “? only works with Result.” It works with any type implementing FromResidual — in practice, Result<T, E> and Option<T>, but never a mix of the two in a single call without converting first.
  • “? swallows the error if the types don’t match.” The opposite — a type mismatch with no From impl is a compile error (E0277), not a silent runtime failure. Rust would rather refuse to build than guess how to convert your error.
  • “Returning Err from main is basically the same as panic!.” It is not — there is no unwinding table walked, no thread 'main' panicked at ... line, and (as shown above) local variables are dropped through a completely ordinary function return, not through a panic’s unwind machinery. The only things they share are “the program stops” and a nonzero-ish exit status.

AI assistants sometimes erase a specific, actionable error into Box<dyn Error> via ? one level too early — inside a function whose caller genuinely needs to distinguish which failure occurred, not just that failure occurred at all.

🐛 Spot the bug

fn load_user(id: u32) -> Result<String, Box<dyn std::error::Error>> {
let raw = std::fs::read_to_string(format!("users/{id}.json"))?;
Ok(raw)
}
fn handle_request(id: u32) {
match load_user(id) {
Ok(user) => println!("loaded: {}", user),
Err(_) => println!("something went wrong"), // cannot tell "not found" from "disk full"
}
}
Show the answer

load_user erases every possible failure — a missing file, a permissions error, a full disk — into the same opaque Box<dyn Error>, and handle_request cannot recover which one happened without downcasting (the awkward pattern covered in the Custom Errors lesson). Since this function has exactly one caller in this codebase and the caller clearly wants to distinguish “no such user” from “something else broke,” a small enum LoadUserError { NotFound(u32), Io(std::io::Error) } with a From<std::io::Error> impl lets ? keep working and lets handle_request match on the real cause.

What does ? do when applied to an Err(e) value?
What constraint must the enclosing function satisfy for ? to compile?
What type is typically used in main() -> Result<(), ...> to accept any error?
How does ? handle mismatched error types between the caller and callee?
What happens when main() -> Result<(), Box<dyn Error>> returns Err(e)?
What does this print, and in what order?
struct Guard;
impl Drop for Guard {
    fn drop(&mut self) { println!("dropping guard"); }
}
fn fails() -> Result<(), String> { Err(String::from("boom")) }
fn main() -> Result<(), String> {
    let _g = Guard;
    fails()?;
    Ok(())
}
Why can't you use ? on a Result directly inside a function that returns Option<T>?
What does this print?
fn first_char_upper(s: &str) -> Option<char> {
    let c = s.chars().next()?;
    Some(c.to_ascii_uppercase())
}
fn main() {
    println!("{:?}", first_char_upper(""));
}