15.5 anyhow: Ergonomic Error Handling for Applications
Alright, let’s talk about anyhow. If thiserror is your meticulous, type-safe blueprint for building known errors, anyhow is the duct tape, WD-40, and the “I’ll deal with that later” box you use when you’re actually running your application. Its entire reason for existence is to make error handling in applications, not libraries, ridiculously ergonomic.
The core problem in application code is often this: you’re calling ten different functions from seven different libraries, each returning their own bespoke enum error type. You don’t really care about the specific variant in your main(); you mostly just want to know something went wrong and print a decent message for the user (or for your logs). Manually converting all those errors into your own type with From is a chore that adds zero value. Enter anyhow.
The anyhow::Result
The star of the show is anyhow::Result<T>. It’s a direct replacement for std::result::Result<T, anyhow::Error>. Its error type is always anyhow::Error, a fantastically flexible container that can hold any error type that implements std::error::Error.
use anyhow::{anyhow, Context, Result};
fn get_user_data() -> Result<String> {
// This function can fail in many ways, but we don't care about the specifics here.
let config = std::fs::read_to_string("config.toml")
.context("Could not read config file")?; // Adds context to the IO error
let user_id: u64 = toml::from_str(&config)
.context("Config file is not valid TOML")?; // Adds context to the TOML parse error
let user_name = some_other_function(user_id)
.context("Failed to retrieve user name from external service")?;
Ok(user_name)
}
See how clean that is? The ? operator automatically converts any std::error::Error-implementing type into an anyhow::Error. It’s a universal error adapter. You’re not writing map_err everywhere; you’re just using ? and moving on with your life.
Context, Context, Context
The most common mistake with anyhow is just letting the low-level error bubble up without any annotation. An error that says “No such file or directory (os error 2)” is technically accurate but useless. Which file? Why were you reading it?
This is where anyhow::Context comes in. The .context() method and its with_context(|| ...) sibling allow you to attach a message to an error as it propagates upwards, wrapping the original source error.
fn load_critical_asset(asset_path: &str) -> Result<String> {
let content = std::fs::read_to_string(asset_path)
.with_context(|| format!("Failed to read critical asset at '{}'", asset_path))?;
if content.is_empty() {
// You can also use `anyhow!` to create a new error from a string.
return Err(anyhow!("Asset file '{}' was empty", asset_path));
}
Ok(content)
}
Now, if this function fails because the file doesn’t exist, your error message might look like this:
Error: Failed to read critical asset at 'textures/default.png'
Caused by: No such file or directory (os error 2)
That is actionable. You see the chain of events immediately. This is the single most important best practice for using anyhow: use .context() liberally at every step to create a stack trace of meaning.
Downcasting: When You Actually Do Care
Sometimes, even in your application, you do need to handle a specific error. Maybe you need to retry on a network timeout or handle a specific invalid input. anyhow hasn’t locked you out. Because it boxes the original error, you can downcast it back to its original type.
fn might_fail() -> Result<()> {
Err(std::io::Error::new(std::io::ErrorKind::PermissionDenied, "oh no!").into())
}
fn main() -> Result<()> {
let err = might_fail().unwrap_err(); // Get the error out
// Try to see if it's a specific error type
if let Some(e) = err.downcast_ref::<std::io::Error>() {
if e.kind() == std::io::ErrorKind::PermissionDenied {
eprintln!("Hey, you need to run this with the right permissions!");
std::process::exit(1);
}
}
// If we didn't handle it above, just bail with the full error chain
Err(err)
}
This is your escape hatch. Use it sparingly, but it’s incredibly powerful when you need it.
The Rough Edges and The Rules of Engagement
- Don’t Use
anyhowin Libraries. I mean it. A library’s public API should expose predictable, specific error types (ideally crafted withthiserror) so the application can know what to expect. Returninganyhow::Resultfrom a library is a cardinal sin; it’s like throwing a box of unsorted, unlabeled parts at your user instead of a finished component. - It’s for Applications.
main, CLI tools, web server request handlers, scripts—these are its home. Anywhere where the primary goal is to do a job, not to provide a perfectly typed API. anyhow!is for Stringly-Typed Errors. Theanyhow!macro is perfect for those one-off error cases where creating a whole customErrorenum variant feels like overkill. It’s your quick-and-dirty, “this should never happen” or “this input is just plain wrong” error.
anyhow embraces the messy reality of application development. It acknowledges that most of the time, failure is something you report and handle generically, not something you need to pattern-match on in fifty different places. It removes the boilerplate and lets you focus on the meaning of the error through context, not just its type. Use it, love it, but for the love of all that is holy, keep it out of your library crates.