Files
Hugo/content/blog/rust-error-handling.md
T
fukunee 1fb7959071 Standardize poet attributions and reformat blog template
Convert all poet attribution lines to use an em dash, English
name format, and consistent spacing. Re-indent the blog single
page template for readability and adjust the typewriter element
layout.
2026-05-14 02:29:10 +08:00

100 lines
3.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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, UK, 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.