Rust Error Handling Patterns

? operator, unwrap, expect, map_err and thiserror patterns with tradeoff notes.

Reference for Rust error-handling idioms — the ? operator, unwrap and expect, map_err, ok_or, custom Error types, thiserror and anyhow — each with a code example and a note on when to reach for it.

What does the ? operator actually do?

On a Result, ? returns the Ok value or returns early with the Err, converting the error via the From trait into the function's error type. On an Option it returns the Some value or returns None early. The function must itself return a compatible Result or Option.

Rust has no exceptions; errors are ordinary values returned as Result<T, E> or Option<T>. The ecosystem has settled on a small set of idioms for propagating and converting them. This tool is a reference of those patterns, each with a worked snippet and a note on when it is the right choice.

How it works

Result<T, E> represents success (Ok) or failure (Err); Option<T> represents presence (Some) or absence (None). The patterns below combine and convert these:

  • Propagate with ? — the workhorse; bubbles errors up, converting via From.
  • Extract or crash with unwrap/expect — fine for tests and proven invariants, dangerous in production.
  • Transform with map_err, ok_or, unwrap_or, unwrap_or_else — adjust the error or supply a fallback.
  • Define custom error enums, generated cheaply with thiserror.
  • Box disparate errors with anyhow for application code.

Worked example

use std::fs;
use thiserror::Error;

#[derive(Error, Debug)]
enum ConfigError {
    #[error("could not read config: {0}")]
    Io(#[from] std::io::Error),   // #[from] enables ? to convert io::Error
    #[error("empty config file")]
    Empty,
}

fn load(path: &str) -> Result<String, ConfigError> {
    let text = fs::read_to_string(path)?; // io::Error -> ConfigError::Io
    if text.is_empty() {
        return Err(ConfigError::Empty);
    }
    Ok(text)
}

For an application binary you might instead use anyhow:

use anyhow::{Context, Result};

fn load(path: &str) -> Result<String> {
    let text = std::fs::read_to_string(path)
        .with_context(|| format!("reading {path}"))?;
    Ok(text)
}

Notes

  • The #[from] attribute on a thiserror variant generates a From impl so ? converts the source error automatically — no manual map_err needed.
  • Reserve unwrap for cases where a failure is genuinely a bug; prefer expect("reason") so the panic message names the broken invariant.
  • unwrap_or_default() is a clean fallback when the type implements Default.
  • Use thiserror in libraries (typed, matchable) and anyhow in applications (boxed, contextual) — mixing them is common and intentional.