Files
Hugo/content/blog/rust-error-handling.md
T

3.3 KiB
Raw Blame History

title, date, draft, poet, description, tags
title date draft poet description tags
Rust Error Handling: From unwrap to anyhow 2026-05-16T13:00:00+08:00 false If you can meet with Triumph and Disaster And treat those two impostors just the same; ——Rudyard Kipling,英国,1865–1936 The journey from panic-driven development to proper error types. When to use thiserror for library crates and anyhow for applications.
Rust
Error Handling
Programming

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

Crate Use Case Key Feature
thiserror Library crates Derive macro, typed errors
anyhow Application binaries Context chains, easy ?
eyre Applications (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.