Beginner's Guide to anyhow and thiserror in Rust
Rust error handling can feel intimidating at first, especially when you want nice error messages without writing a lot of boilerplate. Two crates make this much easier:
thiserrorhelps you define clean, typed errors for libraries and reusable code.anyhowhelps you return ergonomic errors in application code, with great context.
In this post, we will build a tiny config loader using thiserror, then wire it into a main function using anyhow.
Add the Dependencies
Add these crates to your Cargo.toml:
[dependencies]
anyhow = "1"
thiserror = "2"
Use thiserror for a Clear Error Type
When you are writing a library or a module that other code will call, it is useful to return a typed error that callers can match on. thiserror lets you define this error type with minimal code.
Here is a small example that loads a server port from a file:
use std::fs;
use std::num::ParseIntError;
use thiserror::Error;
#[derive(Debug, Error)]
enum ConfigError {
#[error("missing config file at {path}")]
Missing { path: String },
#[error("could not read config file at {path}: {source}")]
ReadFile {
path: String,
source: std::io::Error,
},
#[error("invalid port: {source}")]
InvalidPort { source: ParseIntError },
}
struct Config {
port: u16,
}
fn load_config(path: &str) -> Result<Config, ConfigError> {
let contents = fs::read_to_string(path).map_err(|source| {
if source.kind() == std::io::ErrorKind::NotFound {
ConfigError::Missing {
path: path.to_string(),
}
} else {
ConfigError::ReadFile {
path: path.to_string(),
source,
}
}
})?;
let port = contents
.trim()
.parse::<u16>()
.map_err(|source| ConfigError::InvalidPort { source })?;
Ok(Config { port })
}
This gives you strongly typed errors with readable messages, and it still works with the ? operator.
Use anyhow in Application Code
In application code (like main), you often do not need to match on exact error types. You just want a useful error message with context. That is exactly what anyhow is for.
use anyhow::{Context, Result};
fn main() -> Result<()> {
let config = load_config("app.port")
.with_context(|| "loading server port from app.port")?;
run_server(config.port)
.with_context(|| "starting server")?;
Ok(())
}
fn run_server(port: u16) -> Result<()> {
println!("Server running on http://localhost:{port}");
Ok(())
}
If something fails, anyhow prints a nice error chain:
Error: loading server port from app.port
Caused by:
missing config file at app.port
When to Use Which
- Use
thiserrorin libraries or shared modules where callers might want to match on specific error variants. - Use
anyhowin binaries and application code where you care more about good messages than precise types. - They work great together because
thiserrorimplementsstd::error::Error, whichanyhowcan wrap automatically.
Quick Recap
thiserrorgives you clean, typed errors with readable messages.anyhowgives you easy error propagation plus context in apps.- Combine them: typed errors inside, ergonomic errors at the top.