15.6 thiserror for Libraries vs anyhow for Binaries
Alright, let’s cut through the noise. You’ve got errors, and you’ve got two fantastic tools to deal with them: thiserror and anyhow. Using them effectively isn’t about which one is “better”—it’s about using the right tool for the job. And the job is defined by what you’re building: a reusable library or a final application binary. Get this wrong, and you’ll annoy other developers (if you’re writing a library) or drown in your own frustration (if you’re writing a binary).
The Golden Rule: Library vs. Binary
Here’s the mantra, tattoo it on your forearm: Libraries must expose their own error types; binaries must consume and handle errors. It’s a contract.
When you’re writing a library (my_awesome_parser), you have no idea how I, the consumer of your library, want to handle the errors. Maybe I’m building a CLI and want to print a nice message. Maybe I’m a web server and need to convert it into a 400 Bad Request. You can’t possibly know. So, you give me a structured, predictable error type that I can match on. This is thiserror’s home turf.
When you’re writing a binary (my_cli_tool), you’re at the end of the chain. Errors from all the libraries you use (serde, reqwest, my_awesome_parser) flow up to you, and your job is to deal with them: log them, print them, panic, or send a carrier pigeon. You need a flexible, easy way to collect all these different errors, add context, and bail out. You are the handler. This is anyhow’s kingdom.
Crafting Library Errors with thiserror
thiserror is a sublime macro that automatically implements std::error::Error for your enum. You get all the benefits of a structured error type without the soul-crushing boilerplate. The key is that your error type is an enum, and each variant represents a specific, known failure mode your library can experience.
// This is what a responsible library author writes.
use thiserror::Error;
#[derive(Error, Debug)]
pub enum DatabaseError {
#[error("Failed to connect to database at {0}:{1}")]
ConnectionFailed(String, u16),
#[error("Query execution failed: {0}")]
QueryError(#[from] sqlx::Error),
#[error("Row not found for id: {0}")]
NotFound(i32),
}
See what we did there? The #[error("...")] attribute defines the Display format. The #[from] attribute automatically generates a From<sqlx::Error> conversion, making the ? operator work seamlessly. Now, a user of our library can do this:
fn get_user(&self, id: i32) -> Result<User, DatabaseError> {
let row = query!("SELECT * FROM users WHERE id = $1", id)
.fetch_one(&self.pool)
.await
.map_err(|e| match e {
sqlx::Error::RowNotFound => DatabaseError::NotFound(id),
other => DatabaseError::from(other), // Thanks, #[from]!
})?;
// ...
}
The user gets a precise, predictable error they can program against. They can write match error { DatabaseError::NotFound(id) => ... }. This is a good API. This is what your users deserve.
Wielding anyhow in Binaries
Now, flip the script. You’re writing main.rs. You’re calling five different libraries. You do not care about the specific variant of reqwest::Error you got; you just care that “the HTTP request failed” and you want to add that you were “trying to fetch the user profile.” This is where anyhow! shines.
anyhow::Result<T> is basically a convenient alias for Result<T, anyhow::Error>. Its superpower is that it can wrap any error that implements Error, and it provides ergonomic ways to add context.
// This is what you write in your binary or application code.
use anyhow::{Context, Result};
async fn main() -> Result<()> {
let config = load_config().context("Failed to load config")?;
let client = create_db_client(&config).await.context("Could not connect to the database")?;
let data = fetch_fancy_data(&client).await.context("Failed to acquire the fancy data")?;
process_data(data).context("The data was too fancy to process")?;
Ok(())
}
fn load_config() -> Result<Config> {
// Let's say this returns a `serde_yaml::Error`
let file = std::fs::File::open("config.yaml")?;
let config: Config = serde_yaml::from_reader(file)?;
Ok(config)
}
Look at that main! It’s clean, it’s readable, and every ? automatically converts whatever error type pops up into an anyhow::Error. The .context() method is the killer feature—it adds a string message to the error chain, so when it finally gets printed (with {:#}), you get a beautiful, detailed trace:
Error: Failed to acquire the fancy data
Caused by:
Failed to connect to database at db.prod.com:5432
Caused by:
could not connect to server: Connection timed out
It tells you exactly what happened, and at every step why it was happening. This is a debugging dream. It’s the opposite of the opaque “I/O error” message that makes you want to scream.
The Cardinal Sin and How to Avoid It
The biggest mistake is using anyhow in a library. Just don’t do it. If you publish a library that returns anyhow::Result, you have committed a grave sin against your users. You’ve taken away their ability to handle errors programmatically. All they can do is display it. It’s lazy, and it makes your library unusable for anyone who needs to recover from specific errors.
Conversely, while you can use thiserror in a binary, it’s often overkill. You’ll find yourself doing a lot of manual error mapping from library errors into your own binary’s error type, which is precisely the tedious work anyhow saves you from. Save your energy for the actual logic of your application.
So there you have it. thiserror for building a precise API for others. anyhow for effortlessly handling everything that comes at you. Use them correctly, and you’ll write code that is both robust and a genuine pleasure to work with.