100 lines
3.3 KiB
Markdown
100 lines
3.3 KiB
Markdown
---
|
||
title: "Rust Error Handling: From unwrap to anyhow"
|
||
date: 2026-05-16T13:00:00+08:00
|
||
draft: false
|
||
poet: |
|
||
If you can meet with Triumph and Disaster
|
||
And treat those two impostors just the same;
|
||
——Rudyard Kipling,英国,1865–1936
|
||
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<dyn Error>
|
||
|
||
The quick upgrade path:
|
||
|
||
```rust
|
||
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:
|
||
|
||
```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<T>`, 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.
|