Rust Patterns That Matter #3: Error Handling with ? + thiserror + anyhow
Post 3 of 22 in Rust Patterns That Matter. Companion series: Building a Chat Server in Rust.
Previous: #2: From / Into Conversions | Next: #4: Interior Mutability
Your application reads a config file, parses JSON, queries a database, and serves
HTTP. Each layer produces its own error type. Combining them manually means writing
error enums, Display impls, and From impls for every
source. There's a better way - and the answer depends on whether you're
writing a library or an application.
The problem
fn load_config() -> Result<Config, ???> {
let text = std::fs::read_to_string("config.json")?; // io::Error
let config: Config = serde_json::from_str(&text)?; // serde_json::Error
Ok(config)
}
Two different error types. The ? operator needs to convert both into
one return type. What goes in place of ????
The manual approach
enum ConfigError {
Io(std::io::Error),
Parse(serde_json::Error),
}
impl std::fmt::Display for ConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
ConfigError::Io(e) => write!(f, "IO error: {e}"),
ConfigError::Parse(e) => write!(f, "parse error: {e}"),
}
}
}
impl From<std::io::Error> for ConfigError {
fn from(e: std::io::Error) -> Self { ConfigError::Io(e) }
}
impl From<serde_json::Error> for ConfigError {
fn from(e: serde_json::Error) -> Self { ConfigError::Parse(e) }
}
This works, but it's 20 lines of boilerplate per error type. Add a database layer and an HTTP layer and you're writing more error plumbing than actual logic.
thiserror - for libraries
thiserror is a derive macro that generates the Display and
From impls:
use thiserror::Error;
#[derive(Error, Debug)]
enum ConfigError {
#[error("failed to read config file")]
Io(#[from] std::io::Error),
#[error("failed to parse config")]
Parse(#[from] serde_json::Error),
}
The #[error("...")] attribute generates the Display impl.
The #[from] attribute generates the From impl. What was
20 lines is now 8, and the intent is clear.
The key feature: callers can match on variants.
match load_config() {
Ok(config) => { /* use config */ }
Err(ConfigError::Io(e)) if e.kind() == ErrorKind::NotFound => {
// file missing — use defaults
}
Err(e) => return Err(e.into()),
}
This is why thiserror is for libraries: downstream code can
programmatically handle specific error cases. The error type is part of your API.
anyhow - for applications
In a binary (not a library), you often don't need to match on error variants. You
just want to bubble errors up with context, then display them to the user.
anyhow makes this trivial:
use anyhow::{Context, Result};
fn load_config() -> Result<Config> {
let text = std::fs::read_to_string("config.json")
.context("failed to read config file")?;
let config = serde_json::from_str(&text)
.context("failed to parse config")?;
Ok(config)
}
anyhow::Result<T> is an alias for
Result<T, anyhow::Error>. anyhow::Error wraps
any error type (anything implementing std::error::Error). You don't
define error enums at all.
.context("...") adds a human-readable message. When the error is
displayed, you get a chain:
Error: failed to read config file
Caused by:
No such file or directory (os error 2)
This is exactly what you want in an application: clear error messages with context, minimal boilerplate, and no need to define error types for every module.
The split
This is the idiomatic Rust convention:
- Libraries use
thiserror. They define structured error types that callers can match on. The error type is part of the public API. - Applications (binaries) use
anyhow. They bubble errors up with context messages. Nobody matches on the error - it's displayed to the user.
When the line blurs
Sometimes a library's internal code doesn't need structured errors - it's
private logic that will wrap errors before returning them publicly. Using
anyhow internally and converting to thiserror at the
public boundary is a valid approach.
Conversely, if you find yourself pattern-matching on anyhow::Error
(downcasting to specific types), that's a sign you need structured errors. Switch
to thiserror.
The ? operator ties it together
Everything in this post works because ? calls From::from()
to convert errors (as covered in #2).
thiserror generates the From impls with #[from].
anyhow provides a blanket From<E: Error> for
anyhow::Error. Both patterns are built on the same conversion
machinery.
When to use it
thiserror: any crate published for others to use, any module where callers need to handle specific error cases, any error that's part of a public APIanyhow: binary applications, CLI tools, scripts, anything where errors are displayed to humans rather than matched on by code- Manual enums: when you want full control and don't want the dependency - the pattern is the same, just more verbose
See it in practice: Building a Chat Server #1: Hello, TCP
uses this pattern for handling network and parse errors with ?.
Telex