Day 5 · Concept 15
Custom errors & thiserror
In production, return a real error enum so callers can match on variants. thiserror derives the boilerplate. For application boundaries, anyhow gives you ergonomic Box-of-anything errors.
1 · The hand-rolled version
use std::fmt;
#[derive(Debug)]
enum AppError {
NotFound,
Parse(String),
Io(std::io::Error),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
AppError::NotFound => write!(f, "not found"),
AppError::Parse(s) => write!(f, "parse error: {}", s),
AppError::Io(e) => write!(f, "io error: {}", e),
}
}
}
impl std::error::Error for AppError {}
impl From<std::io::Error> for AppError {
fn from(e: std::io::Error) -> Self { AppError::Io(e) }
}Three impls per variant. Workable; tedious.
2 · The thiserror version
use thiserror::Error;
#[derive(Debug, Error)]
pub enum AppError {
#[error("not found")]
NotFound,
#[error("parse error: {0}")]
Parse(String),
#[error("io error")]
Io(#[from] std::io::Error),
}#[derive(Error)] writes Display + Error;
#[from] writes the From impl. Cargo.toml:
thiserror = "1".
3 · anyhow — for applications, not libraries
use anyhow::{Context, Result};
fn load_config(path: &str) -> Result<String> {
let raw = std::fs::read_to_string(path)
.with_context(|| format!("reading {}", path))?;
Ok(raw)
}
fn main() -> Result<()> {
let cfg = load_config("config.toml")?;
println!("{}", cfg);
Ok(())
}anyhow::Result<T> is Result<T, anyhow::Error> where
anyhow::Error wraps anything. .context() adds a message at each
layer — the resulting error reads like a stack trace.
The rule. Libraries return
Result<T, MyError> using
thiserror so callers can match. Binaries return anyhow::Result
because they only need to log-and-exit.4 · Common mistakes
- Stringly-typed errors (
Result<T, String>). Loses type info. Use an enum. - Mixing
anyhowin a library. Forces every consumer to depend on it. Usethiserror. - Forgetting
#[from]on the variant. Then?can't auto-convert.
5 · When it clicks
- You reach for
thiserrorin libraries andanyhowin binaries automatically. - Every error gets a
.context()at the boundary it crosses. - You think of errors as data — matchable, recoverable — not as exceptions.
Found this useful?