anyhow vs thiserror in Rust: which should you use?
The difference between anyhow and thiserror for Rust error handling, when to use each, with context(), bail!, #[error] and #[from] examples.
Two crates dominate Rust error handling, and they solve different problems. The short version: thiserror for libraries, anyhow for applications, and many projects use both.
thiserror |
anyhow |
|
|---|---|---|
| What it gives you | a derive that writes Display, Error and From for your own error enum |
one catch-all error type, anyhow::Error |
| Callers can match on the error | yes, it's a normal enum | not easily (only by downcasting) |
| Adding context | add fields to variants | .context("...") on any Result |
| Best for | libraries, modules with errors callers handle differently | binaries, CLIs, main, glue code, tests |
thiserror: typed errors without boilerplate
use thiserror::Error;
#[derive(Debug, Error)]
pub enum StoreError {
#[error("item `{0}` not found")]
NotFound(String),
#[error("only {available} left, asked for {requested}")]
OutOfStock { requested: u32, available: u32 },
#[error("invalid quantity")]
BadQuantity(#[from] std::num::ParseIntError),
}
#[error("...")]writes theDisplaymessage, with fields in braces.#[from]writesimpl From<ParseIntError> for StoreError, so?converts automatically, and records it as thesource().- The result is an ordinary enum: callers can
matchand retry onOutOfStock, show a 404 onNotFound, and so on. thiserror doesn't appear in your public API; it only generates code.
anyhow: one error type for the whole application
use anyhow::{bail, Context, Result};
fn load_config(path: &str) -> Result<Config> {
let text = std::fs::read_to_string(path)
.with_context(|| format!("reading {path}"))?;
let config: Config = toml::from_str(&text).context("parsing config")?;
if config.workers == 0 {
bail!("workers must be at least 1");
}
Ok(config)
}
anyhow::Result<T>isResult<T, anyhow::Error>, and?accepts any error type..context(...)/.with_context(|| ...)wrap the error with a message describing what you were doing. Needsuse anyhow::Context;.bail!("...")returns an error immediately;ensure!(cond, "...")bails if the condition is false;anyhow!("...")builds an error value.maincan returnanyhow::Result<()>; on error it prints the whole chain:
Error: reading /etc/arena.toml
Caused by:
No such file or directory (os error 2)
Print a chain yourself with {:#} (one line, causes joined with : ) or {:?} (the multi-line report).
How they fit together
A typical service: each module (database, payments, parsing) defines a thiserror enum that callers inside the codebase can match on. Request handlers and main use anyhow::Result and .context(), converting module errors with ?, and log the full chain at the top. When code really needs to inspect an anyhow::Error, err.downcast_ref::<StoreError>() recovers the typed error.
Common mistakes
- anyhow in a library's public API. Callers lose the ability to handle specific failures. Expose a typed error.
- Forgetting
use anyhow::Context;: then.context()is "no method namedcontextfound". - Context that repeats the cause. Say what you were trying to do ("loading user 42"), not what failed ("file not found"); the cause is already in the chain.
- Formatting with
{}and losing causes.{}prints only the outermost message; use{:#}in logs.
Example
use anyhow::{Context, Result, bail, ensure};
use thiserror::Error;
// A "library" error: callers can match on it.
#[derive(Debug, Error)]
enum StoreError {
#[error("item `{0}` not found")]
NotFound(String),
#[error("only {available} left, asked for {requested}")]
OutOfStock { requested: u32, available: u32 },
#[error("invalid quantity")]
BadQuantity(#[from] std::num::ParseIntError),
}
fn reserve(stock: &[(&str, u32)], item: &str, qty_text: &str) -> Result<u32, StoreError> {
let requested: u32 = qty_text.trim().parse()?; // ParseIntError -> StoreError via #[from]
let (_, available) = stock.iter().find(|(name, _)| *name == item).ok_or_else(|| StoreError::NotFound(item.to_string()))?;
if requested > *available {
return Err(StoreError::OutOfStock { requested, available: *available });
}
Ok(available - requested)
}
// "Application" code: anyhow plus context.
fn handle_order(stock: &[(&str, u32)], line: &str) -> Result<String> {
let (item, qty) = line.split_once(':').context("expected `item:qty`")?;
ensure!(!item.is_empty(), "empty item name");
if item == "banned" {
bail!("item `{item}` cannot be sold");
}
let left = reserve(stock, item, qty).with_context(|| format!("processing order {line:?}"))?;
Ok(format!("reserved {item}, {left} left"))
}
fn main() {
let stock = [("mug", 5), ("tee", 1)];
for line in ["mug:2", "tee:3", "hat:1", "mug:two", "banned:1", "no colon"] {
match handle_order(&stock, line) {
Ok(msg) => println!("ok {msg}"),
Err(e) => {
println!("error {e:#}");
if let Some(StoreError::OutOfStock { available, .. }) = e.downcast_ref::<StoreError>() {
println!(" (could offer {available} instead)");
}
}
}
}
}
Output:
ok reserved mug, 3 left
error processing order "tee:3": only 1 left, asked for 3
(could offer 1 instead)
error processing order "hat:1": item `hat` not found
error processing order "mug:two": invalid quantity: invalid digit found in string
error item `banned` cannot be sold
error expected `item:qty`