---
title: What is the ? operator
summary: 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.
lang: en
slug: what-is-the-operator
published: 2026-04-17T16:32:01+00:00
updated: 2026-04-25T22:00:13+00:00
canonical: https://www.rustfaq.org/en/what-is-the-operator/
---

# What is the ? operator



## The pyramid of doom

You are writing a function that reads a configuration file, parses it as JSON, and validates the fields. Each step can fail. Without a shortcut, your code turns into a pyramid of `match` statements. You nest one `match` inside another, and soon the actual logic is buried under four levels of indentation. The `?` operator exists to flatten that pyramid back into a straight line.

## How the operator actually works

Think of `?` as an automated quality inspector on a factory line. The inspector checks each part as it arrives. If the part passes, the inspector stamps it, removes the protective casing, and lets it move to the next station. If the part fails, the inspector immediately stops the line, packages the defect report, and ships it back to the manager. The rest of the assembly process never runs. In Rust, the "part" is a `Result` or `Option`, the "stamp" is the unwrapped value, and the "manager" is the calling function.

The operator does two things at once. It unwraps the success case so you can use the inner value. It also handles the failure case by returning early from the current function. You do not write the early return yourself. The compiler generates it.

```rust
/// Attempts to parse a string as an integer.
fn parse_id(input: &str) -> Result<i32, std::num::ParseIntError> {
    // The ? operator checks the Result.
    // If Ok, it extracts the i32.
    // If Err, it returns the error immediately.
    let id = input.parse::<i32>()?;
    
    // All steps succeeded. Hand the data back.
    Ok(id)
}
```

Under the hood, `?` expands to a `match` expression. When you write `let x = some_operation()?;`, the compiler translates it to a control flow structure that checks the variant. If it sees `Ok(val)`, it assigns `val` to `x` and continues. If it sees `Err(e)`, it generates a `return Err(e.into());` statement. The rest of the function body becomes unreachable code for that execution path. The compiler optimizes this away completely. There is zero runtime overhead compared to writing the `match` yourself.

Convention aside: developers almost always place `?` at the end of a line, right before the semicolon. It reads like a question mark asking whether the operation succeeded, and it keeps the error propagation visually separate from the success path. Stick to that placement. It is the universal signal in Rust codebases.

## The hidden conversion step

The `?` operator relies on Rust's trait system to handle type mismatches. This is where the magic happens. If your function returns `Result<String, io::Error>` but `File::open` returns `Result<File, io::Error>`, the types match perfectly. The compiler passes the error through unchanged.

If your function returns a custom error type instead, Rust automatically converts the incoming error into your custom type using the `From` trait. You do not need to write the conversion manually. The compiler inserts `.into()` behind the scenes.

```rust
use std::fs;
use std::io;

/// Reads a file and returns its contents as a string.
fn load_config(path: &str) -> Result<String, io::Error> {
    // Opens the file. Fails fast if the path is wrong.
    let mut file = fs::File::open(path)?;
    
    // Creates a buffer for the file contents.
    let mut contents = String::new();
    
    // Reads everything into the buffer.
    // Returns early if the disk is full or the file is truncated.
    file.read_to_string(&mut contents)?;
    
    // All steps succeeded. Hand the data back.
    Ok(contents)
}
```

When you define your own error enum, you implement `From` for each error variant you want to accept. The `?` operator then uses that implementation to wrap the original error inside your enum. This keeps your public API clean while preserving the original error details for debugging. Treat the `From` implementation as a translation layer. If you do not define it, the compiler will not guess how to translate one error into another.

## Working with Option

The `?` operator works identically 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` as well. You cannot mix `Result` and `Option` in the same chain without converting one to the other first.

```rust
/// Finds a user by ID and extracts their age.
fn get_user_age(user_id: u32) -> Option<u32> {
    // Returns None immediately if find_user returns None.
    let user = find_user(user_id)?;
    
    // Returns None immediately if age() returns None.
    let age = user.age()?;
    
    // Both lookups succeeded. Wrap the result.
    Some(age)
}
```

The compiler treats `Option` and `Result` as separate error domains. You cannot propagate a `None` into a function that expects a `Result`, and you cannot propagate an `Err` into a function that expects an `Option`. If you try, the compiler rejects you with E0308 (mismatched types). You must explicitly convert between them using methods like `.ok()` or `.into()`. Keep your error domains consistent. Mixing them without conversion breaks the chain.

## Async and the FromResidual trait

Async functions in Rust return a future that resolves to a `Result` or `Option`. The `?` operator works inside `async fn` exactly like it does in synchronous code. The compiler handles the translation by relying on the `FromResidual` trait. This trait abstracts the early return behavior so the operator works across different return types without duplicating logic.

You do not need to implement `FromResidual` yourself. The standard library provides it for `Result`, `Option`, and async blocks. When you use `?` in an async function, the compiler generates code that pauses the future, returns the error or `None` to the caller, and resumes execution only if the operation succeeded. The mental model stays the same. The operator stops the current task and bubbles the failure upward.

## Common compiler rejections

The `?` operator has strict boundaries. It only works inside functions that return `Result`, `Option`, or a type implementing `FromResidual`. You cannot use it in a function that returns `()` or a plain value. If you try, the compiler rejects you with E0277 (trait bound not satisfied) because there is nowhere for the error to go.

The most common mistake happens in `main`. By default, `main` returns `()`. If you write `File::open("config.txt")?;` inside a standard `main`, the compiler complains that `?` cannot be used in a function that returns `()`. The fix is to change the signature to `fn main() -> Result<(), Box<dyn std::error::Error>>`. This tells the compiler that `main` is allowed to fail, and it will print the error to stderr and exit with a non-zero status code.

Another trap is mismatched error types. The `?` operator relies on `From` to convert errors. If you return `Result<String, MyError>` but try to propagate a `std::io::Error`, the compiler will reject it unless you implement `From<std::io::Error> for MyError`. Without that implementation, you get E0308 (mismatched types). The compiler cannot guess how to translate one error into another. You have to define the mapping explicitly.

A third pitfall involves shadowing. If you use `?` on a variable and then try to use that same variable later, the compiler will reject you with E0382 (use of moved value) if the operation consumes the variable. The `?` operator does not clone or borrow. It takes ownership of the `Result` or `Option`, extracts the value, and discards the wrapper. Plan your variable lifetimes accordingly.

## When to use what

Use `?` when you want to propagate an error up the call stack without handling it locally. Use `match` when you need to recover from a specific error, log it, or transform it into a different success value. Use `unwrap` only in tests or throwaway scripts where a failure means your assumptions are broken. Use `expect` when you need a clear panic message for debugging, but still want to fail fast. Reach for `?` in production code. It keeps error handling consistent and prevents silent failures.

## Where to go next

- [How to Create Custom Error Types in Rust](/en/how-to-create-custom-error-types-in-rust/)
- [What are error handling best practices](/en/what-are-error-handling-best-practices/)
- [How to Catch Panics in Rust with catch_unwind](/en/how-to-catch-panics-in-rust-with-catchunwind/)