--- title: "Rust Error Handling: From unwrap to anyhow" date: 2026-05-16T13:00:00+08:00 draft: false description: "The journey from panic-driven development to proper error types. When to use thiserror for library crates and anyhow for applications." tags: - 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 ```rust 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: ```rust fn do_thing() -> Result<(), Box> { let config = read_config()?; let db = connect(&config)?; query(&db)?; Ok(()) } ``` The `?` operator works with any error type that implements `From`. `Box` 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: ```rust 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`, so `?` works seamlessly. Callers can match on variants: ```rust 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: ```rust 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.