The ? Operator in Rust: Desugaring & vs unwrap/expect

Max WellsMax WellsFounder of Rustify

TL;DR: The ? operator is shorthand for: "if this is Ok(v), give me v; if it's Err(e), convert e with From::from and return it immediately." It works on Result<T, E> and Option<T>. It replaces verbose match blocks and is the standard way to propagate errors in Rust. Functions using ? must return Result or Option. The conversion via From means ? can change error types automatically, the foundation of how thiserror and anyhow work.


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 vReturns vReturns v
On Err(e) / NoneReturns error to callerPanicsPanics with message
Use in production✅ Yes❌ Avoid❌ Avoid
Use in tests✅ Yes✅ Acceptable✅ Acceptable
Requires return typeResult/OptionNoNo

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


  • 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 From impls that power ?

Keep Reading

Ready to Land a $80-120k Rust Job?