Streamlining the Result Type
Rust's approach to error handling via the Result enum is a paradigm shift for developers coming from exception-heavy languages. By forcing developers to acknowledge potential failures, Rust ensures memory safety and predictability. However, as a project grows, implementing the std::error::Error trait manually leads to significant boilerplate. This is where the community-standard crates thiserror and anyhow come into play.
The Library Choice: thiserror
When you are building a library, you want your users to be able to programmatically react to specific error conditions. This requires structured, strongly-typed errors. The thiserror crate provides a convenient derive macro for the standard library's error traits without hiding the underlying types.
With thiserror, you define your error types as enums and use attributes to generate the Display and From implementations. This keeps your library lightweight and compatible with the std::error ecosystem.
use thiserror::Error;
#[derive(Error, Debug)]
pub enum DataStoreError {
#[error("failed to read from disk")]
Io(#[from] std::io::Error),
#[error("invalid configuration: {0}")]
InvalidConfig(String),
#[error("unknown data store error")]
Unknown,
}
In the example above, #[from] automatically implements the conversion from std::io::Error to DataStoreError::Io, allowing you to use the ? operator seamlessly.
The Application Choice: anyhow
In application code—like a CLI tool or a web server—you often care less about distinguishing between specific error variants and more about reporting what went wrong to the user or logs. anyhow is designed for this use case. It provides a dynamic error type (anyhow::Error) that can wrap any type implementing std::error::Error.
The main advantage of anyhow is its ability to add context to errors as they bubble up the stack. This makes debugging much easier by providing a trace of what the application was doing when the failure occurred.
use anyhow::{Context, Result};
use std::fs;
fn read_config() -> Result {
let content = fs::read_to_string("config.toml")
.context("failed to load the configuration file from disk")?;
Ok(content)
}
fn main() -> Result<()> {
let config = read_config()?;
println!("Config: {}", config);
Ok(())
}
Which One Should You Use?
The rule of thumb is simple: use thiserror for libraries and anyhow for applications.
- Use thiserror when you are writing a crate that others will consume. It allows your users to match on specific error variants (e.g.,
match err { MyError::NotFound => ... }). - Use anyhow when you are writing a binary. It simplifies error propagation and allows you to use the
?operator across many different error types without defining custom enums for every small function.
By using these two tools effectively, you maintain the rigor of Rust's error handling while drastically reducing the friction of writing and maintaining failure paths in your code.