Rust Error Handling with anyhow and thiserror
Master Rust error handling with anyhow for applications and thiserror for libraries. Practical patterns, conversions, and context-rich error reporting.
What you'll learn
- ✓When to use anyhow vs thiserror
- ✓Create custom error types with thiserror derive macros
- ✓Use anyhow for ergonomic error propagation in applications
- ✓Add context to errors for better debugging
Prerequisites
- •Rust basics — see /blog/rust-variables-and-types
- •Result and Option types — see /blog/rust-error-handling-with-result
Rust’s standard library gives you Result<T, E> and the ? operator for error handling, but building real applications quickly reveals gaps. Defining custom error types requires boilerplate. Propagating errors across different libraries means writing From implementations. Adding context to errors so you can debug failures in production requires even more code.
Two crates solve these problems cleanly: thiserror for defining error types and anyhow for propagating them. They work together or independently, and they are the de facto standard in the Rust ecosystem.
The Rule of Thumb
- Libraries should use
thiserrorto define structured error types that callers can match on. - Applications should use
anyhowto collect and propagate errors from different sources with minimal boilerplate.
This is not a hard rule — some applications use thiserror, some libraries use anyhow internally — but it is a useful starting point.
Setting Up
Add both to your project:
[dependencies]
anyhow = "1"
thiserror = "2"
thiserror: Custom Error Types Without Boilerplate
Without thiserror, defining an error enum that implements Display, Error, and From is tedious:
use std::fmt;
use std::io;
use std::num::ParseIntError;
#[derive(Debug)]
enum AppError {
Io(io::Error),
Parse(ParseIntError),
NotFound(String),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::Io(e) => write!(f, "IO error: {}", e),
AppError::Parse(e) => write!(f, "Parse error: {}", e),
AppError::NotFound(name) => write!(f, "{} not found", name),
}
}
}
impl std::error::Error for AppError {}
impl From<io::Error> for AppError {
fn from(e: io::Error) -> Self { AppError::Io(e) }
}
impl From<ParseIntError> for AppError {
fn from(e: ParseIntError) -> Self { AppError::Parse(e) }
}
That is 30 lines of glue code. With thiserror, the same thing is:
use thiserror::Error;
#[derive(Debug, Error)]
enum AppError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Parse error: {0}")]
Parse(#[from] std::num::ParseIntError),
#[error("{0} not found")]
NotFound(String),
}
The #[error("...")] attribute generates Display. The #[from] attribute generates From implementations. The Error trait is derived automatically.
Using thiserror in Practice
use std::fs;
use thiserror::Error;
#[derive(Debug, Error)]
enum ConfigError {
#[error("failed to read config file: {0}")]
ReadFile(#[from] std::io::Error),
#[error("invalid port number: {0}")]
InvalidPort(#[from] std::num::ParseIntError),
#[error("missing required field: {field}")]
MissingField { field: String },
#[error("port {port} is out of range (1-65535)")]
PortOutOfRange { port: u32 },
}
struct Config {
host: String,
port: u16,
}
fn load_config(path: &str) -> Result<Config, ConfigError> {
let content = fs::read_to_string(path)?; // auto-converts io::Error
let mut host = None;
let mut port = None;
for line in content.lines() {
let parts: Vec<&str> = line.splitn(2, '=').collect();
if parts.len() != 2 { continue; }
match parts[0].trim() {
"host" => host = Some(parts[1].trim().to_string()),
"port" => {
let p: u32 = parts[1].trim().parse()?; // auto-converts ParseIntError
if p == 0 || p > 65535 {
return Err(ConfigError::PortOutOfRange { port: p });
}
port = Some(p as u16);
}
_ => {}
}
}
Ok(Config {
host: host.ok_or(ConfigError::MissingField {
field: "host".to_string(),
})?,
port: port.ok_or(ConfigError::MissingField {
field: "port".to_string(),
})?,
})
}
Callers can pattern-match on ConfigError to handle specific cases:
fn main() {
match load_config("app.conf") {
Ok(config) => println!("Loaded: {}:{}", config.host, config.port),
Err(ConfigError::MissingField { field }) => {
eprintln!("Please add '{}' to your config file", field);
}
Err(ConfigError::ReadFile(e)) if e.kind() == std::io::ErrorKind::NotFound => {
eprintln!("Config file not found, using defaults");
}
Err(e) => eprintln!("Config error: {}", e),
}
}
Advanced thiserror Features
You can use #[source] to set the error source without generating a From impl, and #[backtrace] to capture backtraces:
use thiserror::Error;
#[derive(Debug, Error)]
enum DatabaseError {
#[error("connection failed to {host}:{port}")]
ConnectionFailed {
host: String,
port: u16,
#[source]
cause: std::io::Error,
},
#[error("query failed: {query}")]
QueryFailed {
query: String,
#[source]
cause: Box<dyn std::error::Error + Send + Sync>,
},
}
The #[source] attribute links the error chain so tools like anyhow can print the full cause chain.
anyhow: Ergonomic Error Propagation
While thiserror helps you define errors, anyhow helps you propagate them. The anyhow::Result<T> type is an alias for Result<T, anyhow::Error>, where anyhow::Error can hold any error type that implements std::error::Error.
use anyhow::{Context, Result};
use std::fs;
fn read_config() -> Result<String> {
let content = fs::read_to_string("config.toml")
.context("failed to read config.toml")?;
Ok(content)
}
fn get_port() -> Result<u16> {
let config = read_config()?;
let port: u16 = config
.lines()
.find(|l| l.starts_with("port"))
.context("port not found in config")?
.split('=')
.nth(1)
.context("invalid port line format")?
.trim()
.parse()
.context("port is not a valid number")?;
Ok(port)
}
fn main() -> Result<()> {
let port = get_port()?;
println!("Server starting on port {}", port);
Ok(())
}
When this fails, you get a rich error chain:
Error: port is not a valid number
Caused by:
invalid digit found in string
Adding Context
The .context() method is the killer feature of anyhow. It wraps an error with a human-readable message that explains what you were trying to do:
use anyhow::{Context, Result};
use std::fs;
fn process_user_file(user_id: u64) -> Result<Vec<String>> {
let path = format!("/data/users/{}.json", user_id);
let content = fs::read_to_string(&path)
.with_context(|| format!("failed to read user file for user {}", user_id))?;
let lines: Vec<String> = serde_json::from_str(&content)
.with_context(|| format!("failed to parse JSON for user {}", user_id))?;
Ok(lines)
}
Use .context("static string") when the message is fixed, and .with_context(|| format!(...)) when you need to include dynamic values. The closure version avoids formatting the string when there is no error.
anyhow::bail! and anyhow::ensure!
For early returns with error messages, anyhow provides convenient macros:
use anyhow::{bail, ensure, Result};
fn validate_age(age: i32) -> Result<()> {
ensure!(age > 0, "age must be positive, got {}", age);
ensure!(age < 150, "age {} is unrealistic", age);
Ok(())
}
fn process_command(cmd: &str) -> Result<String> {
match cmd {
"hello" => Ok("Hello, World!".to_string()),
"time" => Ok(format!("Current timestamp: {}", std::time::UNIX_EPOCH.elapsed()?.as_secs())),
other => bail!("unknown command: {}", other),
}
}
fn main() -> Result<()> {
validate_age(25)?;
let response = process_command("hello")?;
println!("{}", response);
Ok(())
}
bail! is equivalent to return Err(anyhow::anyhow!(...)). ensure! is equivalent to if !condition { bail!(...) }.
Combining anyhow and thiserror
The most powerful pattern uses both crates together. Define your library errors with thiserror, and use anyhow in your application code:
// In your library crate
use thiserror::Error;
#[derive(Debug, Error)]
pub enum StorageError {
#[error("key not found: {0}")]
NotFound(String),
#[error("storage is full (capacity: {capacity})")]
Full { capacity: usize },
#[error("corruption detected in block {block}")]
Corrupted { block: u64 },
}
pub struct Storage {
data: std::collections::HashMap<String, Vec<u8>>,
capacity: usize,
}
impl Storage {
pub fn new(capacity: usize) -> Self {
Storage {
data: std::collections::HashMap::new(),
capacity,
}
}
pub fn get(&self, key: &str) -> Result<&[u8], StorageError> {
self.data
.get(key)
.map(|v| v.as_slice())
.ok_or_else(|| StorageError::NotFound(key.to_string()))
}
pub fn put(&mut self, key: String, value: Vec<u8>) -> Result<(), StorageError> {
if self.data.len() >= self.capacity {
return Err(StorageError::Full {
capacity: self.capacity,
});
}
self.data.insert(key, value);
Ok(())
}
}
// In your application crate
use anyhow::{Context, Result};
fn run_app() -> Result<()> {
let mut storage = Storage::new(100);
storage
.put("user:1".into(), b"Alice".to_vec())
.context("failed to store user")?;
let user = storage
.get("user:1")
.context("failed to load user")?;
let name = std::str::from_utf8(user)
.context("user data is not valid UTF-8")?;
println!("User: {}", name);
Ok(())
}
fn main() {
if let Err(e) = run_app() {
eprintln!("Error: {:#}", e); // {:#} prints the full chain
std::process::exit(1);
}
}
Downcasting anyhow Errors
Sometimes application code needs to check if an anyhow error is a specific type. Use downcast_ref:
use anyhow::Result;
fn handle_result(result: Result<()>) {
if let Err(e) = result {
if let Some(storage_err) = e.downcast_ref::<StorageError>() {
match storage_err {
StorageError::NotFound(key) => {
println!("Creating missing key: {}", key);
}
StorageError::Full { capacity } => {
println!("Storage full at {} items, triggering cleanup", capacity);
}
_ => eprintln!("Storage error: {}", storage_err),
}
} else {
eprintln!("Unexpected error: {:#}", e);
}
}
}
Error Handling Patterns
Converting Between Error Types
When you have multiple error types and need to unify them without anyhow:
use thiserror::Error;
#[derive(Debug, Error)]
enum ApiError {
#[error("database error: {0}")]
Database(#[from] DatabaseError),
#[error("validation error: {0}")]
Validation(#[from] ValidationError),
#[error("internal error: {0}")]
Internal(String),
}
#[derive(Debug, Error)]
enum DatabaseError {
#[error("connection lost")]
ConnectionLost,
}
#[derive(Debug, Error)]
enum ValidationError {
#[error("field {field} is required")]
Required { field: String },
}
The #[from] attributes generate From implementations, so ? converts automatically.
Attaching Structured Data to Errors
use thiserror::Error;
#[derive(Debug, Error)]
#[error("request failed: {method} {url} returned {status}")]
struct HttpError {
method: String,
url: String,
status: u16,
body: String,
}
fn make_request(url: &str) -> Result<String, HttpError> {
// Simulated failure
Err(HttpError {
method: "GET".to_string(),
url: url.to_string(),
status: 503,
body: "Service Unavailable".to_string(),
})
}
Wrapping Up
Rust error handling follows a clear pattern once you know the tools:
- Use
thiserrorto define error enums with derivedDisplay,Error, andFromimplementations. This is ideal for libraries where callers need to match on specific error variants. - Use
anyhowfor application code where you care about reporting errors, not matching on them. The.context()method is essential for debuggable error messages. - Combine both: thiserror for your error types, anyhow for propagation and context.
- Use
bail!andensure!for concise early returns with error messages. - Format with
{:#}to print the full error chain.
Start with anyhow in your application, introduce thiserror when you need callers to distinguish between error kinds, and you will have clean, maintainable error handling throughout your Rust codebase.
Related articles
- Rust Rust Error Handling with thiserror and anyhow
Learn practical Rust error handling using thiserror for library errors and anyhow for application errors, with real-world patterns and examples.
- Rust Error Handling with Result and Option in Rust
Handle errors idiomatically in Rust — Result, Option, the ? operator, custom error types, and the thiserror/anyhow ecosystem.
- Rust Rust Error Handling with Result
How idiomatic Rust handles errors with Result and the ? operator: propagation, conversion, custom error types, and when to use anyhow or thiserror.
- Rust Rust Error Handling with Result and the ? Operator
A practical guide to error handling in Rust covering Result, the ? operator, unwrap and expect, custom error types, and the thiserror and anyhow crates.