跳至内容

Rust Error Handling: From unwrap to anyhow

2026-05-16

Rust forces you to think about errors. Starting with unwrap() everywhere is fine for prototypes, but production code needs proper error types.

Stage 1: The Unwrap Phase

let config = read_config().unwrap();
let db = connect(&config).unwrap();
let result = query(&db).unwrap();

Every Rust beginner goes through this. It’s fine for scripts and prototypes — if something fails, the panic message with a line number is good enough. The problem starts when you need graceful error handling or meaningful error messages for users.

Stage 2: Box

The quick upgrade path:

fn do_thing() -> Result<(), Box<dyn std::error::Error>> {
    let config = read_config()?;
    let db = connect(&config)?;
    query(&db)?;
    Ok(())
}

The ? operator works with any error type that implements From<T>. Box<dyn Error> catches everything. But you lose type information — the caller can’t match on specific error variants.

Stage 3: Proper Error Types with thiserror

For libraries, define explicit error types:

use thiserror::Error;

#[derive(Error, Debug)]
pub enum AppError {
    #[error("configuration error: {0}")]
    Config(String),
    #[error("database error: {0}")]
    Database(#[from] sqlx::Error),
    #[error("not found: {0}")]
    NotFound(String),
}

The #[from] attribute auto-implements From<T>, so ? works seamlessly. Callers can match on variants:

match do_thing().await {
    Err(AppError::NotFound(id)) => { /* handle 404 */ }
    Err(e) => { /* log and return 500 */ }
    Ok(data) => { /* success */ }
}

Stage 4: Application Errors with anyhow

For binaries, anyhow provides flexible error handling without the ceremony:

use anyhow::{Context, Result};

fn main() -> Result<()> {
    let config = read_config()
        .with_context(|| "failed to read config file")?;
    let db = connect(&config)
        .context("database connection failed")?;
    Ok(())
}

context() and with_context() attach human-readable messages at each fallible step. The error output is a chain of contexts, making debugging straightforward.

The Ecosystem Split

CrateUse CaseKey Feature
thiserrorLibrary cratesDerive macro, typed errors
anyhowApplication binariesContext chains, easy ?
eyreApplications (alternative)Colorful error reports, custom hooks

The ecosystem has settled on this pragmatic split: thiserror for libraries, anyhow for binaries. Libraries expose typed errors so callers can react programmatically; applications benefit from rich, human-readable error chains.

Remember: unwrap() and expect() still have their place — in tests, in examples, and when an invariant violation truly merits a panic. But for production code paths, make errors part of your type signature.