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.
What you'll learn
- ✓When to use thiserror vs anyhow
- ✓How to define custom error types with thiserror
- ✓How anyhow simplifies application error handling
- ✓Patterns for converting between error types
Prerequisites
- •Basic Rust knowledge
- •Understanding of Result and the ? operator
Rust’s standard library gives you Result<T, E> and the ? operator, but it leaves the error type design up to you. Two crates dominate the ecosystem: thiserror for defining structured error types in libraries, and anyhow for quick, flexible error handling in applications. This guide shows you how and when to use each.
The Problem with Manual Error Types
Without helper crates, defining a proper error type requires implementing Display, Error, and From for every source error:
use std::fmt;
use std::io;
use std::num::ParseIntError;
#[derive(Debug)]
enum AppError {
Io(io::Error),
Parse(ParseIntError),
Custom(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::Custom(msg) => write!(f, "{msg}"),
}
}
}
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 a lot of boilerplate for three variants. thiserror eliminates it.
thiserror: Structured Errors for Libraries
thiserror is a derive macro that generates Display, Error, and From implementations from attributes:
use thiserror::Error;
#[derive(Debug, Error)]
enum DatabaseError {
#[error("connection failed: {0}")]
Connection(String),
#[error("query failed: {query}")]
Query {
query: String,
#[source]
cause: sqlx::Error,
},
#[error("record not found: {table}/{id}")]
NotFound { table: String, id: u64 },
#[error(transparent)]
Io(#[from] std::io::Error),
#[error("migration failed")]
Migration(#[from] MigrationError),
}
#[derive(Debug, Error)]
#[error("migration {version} failed: {reason}")]
struct MigrationError {
version: u32,
reason: String,
}
Key attributes:
#[error("...")]generates theDisplayimplementation. You can interpolate fields by name or position.#[from]generates aFromimplementation and marks the field as the error source.#[source]marks a field as the error source without generatingFrom.#[error(transparent)]delegates bothDisplayandsource()to the inner error.
Using thiserror in a Library
use thiserror::Error;
use std::io;
#[derive(Debug, Error)]
pub enum ConfigError {
#[error("failed to read config file: {path}")]
ReadFile {
path: String,
#[source]
cause: io::Error,
},
#[error("invalid config: {0}")]
Invalid(String),
#[error("missing required field: {0}")]
MissingField(&'static str),
}
pub fn load_config(path: &str) -> Result<Config, ConfigError> {
let content = std::fs::read_to_string(path).map_err(|e| ConfigError::ReadFile {
path: path.to_string(),
cause: e,
})?;
if content.is_empty() {
return Err(ConfigError::Invalid("file is empty".into()));
}
parse_config(&content)
}
pub struct Config {
pub host: String,
pub port: u16,
}
fn parse_config(content: &str) -> Result<Config, ConfigError> {
// Simplified parsing
let host = content
.lines()
.find(|l| l.starts_with("host="))
.map(|l| l.trim_start_matches("host=").to_string())
.ok_or(ConfigError::MissingField("host"))?;
Ok(Config { host, port: 8080 })
}
Callers can match on specific variants and access structured information:
fn main() {
match load_config("app.conf") {
Ok(config) => println!("Host: {}", config.host),
Err(ConfigError::MissingField(field)) => {
eprintln!("Add '{field}' to your config file");
}
Err(e) => eprintln!("Config error: {e}"),
}
}
anyhow: Flexible Errors for Applications
anyhow takes a different approach. Instead of defining error types, you use anyhow::Result<T> which wraps any error implementing std::error::Error. It is designed for application code where you care about displaying errors, not matching on them.
use anyhow::{Context, Result};
fn read_config(path: &str) -> Result<String> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("failed to read config from {path}"))?;
Ok(content)
}
fn parse_port(s: &str) -> Result<u16> {
let port: u16 = s.parse()
.with_context(|| format!("invalid port number: {s}"))?;
anyhow::ensure!(port > 0, "port must be positive, got {port}");
Ok(port)
}
fn main() -> Result<()> {
let content = read_config("app.conf")?;
let port = parse_port("8080")?;
println!("Port: {port}");
Ok(())
}
Key features of anyhow:
anyhow::Result<T>is an alias forResult<T, anyhow::Error>. It accepts any error type..context()and.with_context()add human-readable context to errors, building a chain.anyhow::bail!("msg")is shorthand forreturn Err(anyhow::anyhow!("msg")).anyhow::ensure!(condition, "msg")is likeassert!but returns an error instead of panicking.
Error Chain Display
When you stack contexts, anyhow shows the full chain:
use anyhow::{Context, Result};
fn connect_db(url: &str) -> Result<()> {
Err(std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "connection refused"))
.with_context(|| format!("failed to connect to database at {url}"))?;
Ok(())
}
fn init_app() -> Result<()> {
connect_db("postgres://localhost/mydb")
.context("application initialization failed")?;
Ok(())
}
fn main() {
if let Err(e) = init_app() {
// Prints the full chain:
// Error: application initialization failed
//
// Caused by:
// 0: failed to connect to database at postgres://localhost/mydb
// 1: connection refused
eprintln!("Error: {e:?}");
}
}
When to Use Which
The guideline is simple:
Use thiserror in libraries. Library consumers need to match on error variants, inspect structured fields, and handle specific failure modes. thiserror makes defining these types easy without losing type information.
Use anyhow in applications. Application code typically logs or displays errors rather than matching on them. anyhow lets you propagate any error with ? and add context without defining custom types.
You can use both together. A library defines errors with thiserror, and the application wraps them with anyhow:
// In the library
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ApiError {
#[error("rate limited, retry after {retry_after}s")]
RateLimited { retry_after: u64 },
#[error("authentication failed")]
Unauthorized,
#[error("request failed: {0}")]
Network(#[from] reqwest::Error),
}
// In the application
use anyhow::{Context, Result};
fn fetch_data() -> Result<String> {
let response = api_client::get("/data")
.context("failed to fetch data from API")?;
Ok(response)
}
Downcasting with anyhow
When you need to handle specific error types from anyhow, use downcasting:
use anyhow::Result;
fn process() -> Result<()> {
// ... some operation that might fail
Ok(())
}
fn main() {
if let Err(e) = process() {
// Try to downcast to a specific error type
if let Some(io_err) = e.downcast_ref::<std::io::Error>() {
match io_err.kind() {
std::io::ErrorKind::NotFound => eprintln!("File not found"),
std::io::ErrorKind::PermissionDenied => eprintln!("Permission denied"),
_ => eprintln!("IO error: {io_err}"),
}
} else {
eprintln!("Error: {e:?}");
}
}
}
Wrapping Up
Rust’s error handling ecosystem is built on a clean split: thiserror generates the boilerplate for structured error types that library consumers can match on, while anyhow provides a flexible, context-rich error type for application code where display matters more than matching. Use thiserror when you are writing a library, anyhow when you are writing an application, and combine them freely at the boundary between the two.
Related articles
- Rust 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.
- 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.