rust-errors-thiserror-anyhow
Chooses and applies the correct error-handling crate. thiserror and anyhow are not competitors: they serve opposite sides of the API boundary. thiserror builds structured error types whose callers can match on variants and recover. anyhow builds opaque error values whose callers only display the message and the source chain. Picking the wrong one leaks an unmatchable error type into a library API or buries a binary in pointless boilerplate.
Cross-references: [[rust-impl-error-handling]] (the ? operator, Result vs panic!, custom error types from scratch), [[rust-errors-runtime]] (panics, unwrap/expect, Box<dyn Error>), [[rust-syntax-traits]] (the std::error::Error trait, From, derive macros), [[rust-impl-cargo-project]] (adding crate dependencies, library vs binary crates).
When to use this skill
- The user asks "which error crate should I use" or "thiserror vs anyhow".
- A library crate needs a public error type whose callers must match on causes.
- A binary or application crate needs
fn main() -> Result<()>and short error glue. - An error message no longer carries its underlying cause (broken source chain).
- The user wants to recover a concrete error type out of an opaque
anyhow::Error. - A project mixes both crates and the boundary between them is unclear.
- The user wants to migrate code from
anyhowtothiserroror the reverse.
The one rule behind the choice
thiserroris for errors the caller acts on.anyhowis for errors the caller only reports.
A library cannot know how its callers will handle failures, so it MUST expose a structured type. An application is the final caller, so it can collapse every failure into one opaque type. ALWAYS pick the crate from the caller's needs, never from convenience at the call site.
Decision table
| Crate type | Use | Error crate | Public error type |
|---|---|---|---|
Library (--lib, published, reused) |
callers match on variants and recover | thiserror |
a thiserror-derived enum |
Binary / application (--bin, main) |
callers (humans) display message + chain | anyhow |
anyhow::Error (internal only) |
| Library boundary inside a big app | internal modules expose structured errors | thiserror per module |
module-local enum |
App boundary (main, request handlers) |
collect every module error, add context | anyhow |
anyhow::Error |
ALWAYS use thiserror for library crates. NEVER expose anyhow::Error in a library's public API: the caller then cannot match on the failure, only print it.
ALWAYS use anyhow for the top-level error type of a binary. NEVER hand-write a thiserror enum for a binary's main return type when no caller ever matches on it: that is boilerplate with zero benefit.
thiserror: structured library errors
thiserror is a derive macro. It generates Display and std::error::Error impls so the type behaves exactly as if those impls were hand-written. The crate itself never appears in the public API. Current version: 2.x (thiserror = "2").
Minimal pattern
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ConfigError {
#[error("config file not found at {path}")]
NotFound { path: String },
#[error("config field `{0}` is missing")]
MissingField(String),
#[error("failed to read config file")]
Io(#[from] std::io::Error),
#[error(transparent)]
Parse(#[from] toml::de::Error),
}
#[derive(Error, Debug)] is mandatory: std::error::Error requires Debug as a supertrait, so the derive ALWAYS needs Debug next to Error.
Attribute reference
| Attribute | Generates / does |
|---|---|
#[error("text {field}")] |
the Display impl, with {field} / {0} interpolation |
#[error("{0:?}")] |
interpolates with the Debug formatter instead of Display |
#[from] |
a From<Inner> impl AND marks that field as the source; field must be the only one |
#[source] |
marks a field as the underlying cause returned by Error::source(); no From |
a field literally named source |
treated as #[source] automatically |
#[error(transparent)] |
delegates Display and source() to the single wrapped error, adds no text |
#[from] implies #[source]. Use #[from] when the conversion should be automatic via ?. Use #[source] when you build the variant manually but still want the cause chained. Use #[error(transparent)] for a pure pass-through variant (one inner error, no extra message).
Why the source chain matters
The ? operator calls From::from to convert errors. A #[from] variant lets a foreign error flow through ? into your enum while the original error stays reachable via Error::source(). NEVER format the inner error into the message string instead. #[error("io failed: {0}")] with a plain field loses nothing, but #[error("io failed")] on a field that is NOT #[source]/#[from] discards the cause: tools that walk .source() see a dead end.
See references/methods.md for every attribute with examples and references/examples.md for full enum patterns.
anyhow: opaque application errors
anyhow provides one dynamic error type that absorbs any error. Current version: 1.x (anyhow = "1").
Minimal pattern
use anyhow::{Context, Result, bail, ensure};
fn load_settings(path: &str) -> Result<Settings> {
let raw = std::fs::read_to_string(path)
.with_context(|| format!("reading settings from {path}"))?;
ensure!(!raw.is_empty(), "settings file {path} is empty");
let settings: Settings = toml::from_str(&raw)
.context("parsing settings as TOML")?;
if settings.workers == 0 {
bail!("settings.workers must be at least 1");
}
Ok(settings)
}
fn main() -> Result<()> {
let settings = load_settings("config.toml")?;
run(&settings)
}
API reference
| Item | Purpose |
|---|---|
anyhow::Result<T> |
alias for Result<T, anyhow::Error> |
anyhow::Error |
wraps any E: std::error::Error + Send + Sync + 'static |
.context("msg") |
attach a static-ish context message (eager) |
.with_context(|| ...) |
attach context built lazily, only on the error path |
bail!("msg") |
return Err(anyhow!("msg")) early |
ensure!(cond, "msg") |
return an error early if cond is false |
anyhow!("msg") |
construct an ad-hoc error value |
.downcast_ref::<T>() |
borrow the concrete error type back out, if it is T |
Any error with Error + Send + Sync + 'static converts into anyhow::Error automatically through ?. ALWAYS propagate with ? plus .context(...), NEVER .unwrap() an anyhow::Result: .unwrap() panics and throws away the very context chain anyhow exists to build.
Use .with_context when the message needs format! or other work; use .context for a plain string. The lazy form avoids formatting on the success path.
Recovering a concrete type with downcast
Even though anyhow::Error is opaque, the original typed error is still inside it:
fn handle(err: anyhow::Error) {
if let Some(io) = err.downcast_ref::<std::io::Error>() {
if io.kind() == std::io::ErrorKind::NotFound {
// recover specifically from a missing file
}
}
}
downcast_ref is the escape hatch for the rare case an application must branch on a cause. If branching is common and structured, that is the signal the type belongs in a thiserror enum instead.
Combining both crates
The standard layout of a real project uses both, each on its own side of the boundary:
- Each library crate / internal module defines its own
thiserrorenum and returnsResult<T, ThatEnum>. Variants carry#[from]for foreign errors so?composes cleanly. - The application layer (
main, request handlers, job runners) returnsanyhow::Result<T>. It calls the libraries, and every library enum converts intoanyhow::Errorthrough?because athiserrorenum implementsstd::error::Error. - The application adds human context with
.context(...)as errors bubble up towardmain.
// library crate `store`
#[derive(thiserror::Error, Debug)]
pub enum StoreError {
#[error("record {id} not found")]
NotFound { id: u64 },
#[error(transparent)]
Db(#[from] sqlx::Error),
}
// application crate
use anyhow::{Context, Result};
fn run() -> Result<()> {
let user = store::fetch_user(42) // Result<_, StoreError>
.context("loading the active user")?; // -> anyhow::Error here
Ok(())
}
The library never mentions anyhow; the application never hand-rolls an error enum. See references/examples.md for a full multi-crate walkthrough.
Migration paths
anyhow to thiserror (a binary grows into a library)
Triggered when callers start needing to match on failures. Steps:
- Enumerate every distinct failure the function can produce.
- Define a
#[derive(Error, Debug)]enum with one variant per failure. - Replace each
bail!/anyhow!site withreturn Err(MyError::Variant ...). - Replace
#[from]-able foreign errors so?still composes. - Change the signature from
anyhow::Result<T>toResult<T, MyError>. - Move any leftover
.context(...)into a descriptive variant or a#[source]field.
thiserror to anyhow (a layer becomes app-only)
Triggered when an enum's variants are never matched, only displayed. Steps:
- Confirm no external caller matches on the enum (search for
matchon it). - Change the signature to
anyhow::Result<T>. - Delete the enum; foreign errors already convert through
?. - Re-add lost messages as
.context(...)at each call site.
NEVER migrate a still-public library error type to anyhow: that breaks every downstream caller that matched on it.
eyre and color-eyre (anyhow-family alternatives)
eyre is an anyhow fork with the same API (eyre::Result, .wrap_err(...) instead of .context(...), eyre!, bail!, ensure!) but a customizable report handler. color-eyre plugs into eyre to render colorized, sectioned error reports with spans and suggestions, which is valued for CLI tools. Choose eyre + color-eyre over anyhow only when richer human-facing error reports justify the extra dependency. The decision rule is unchanged: still an application-only opaque error type, NEVER a library's public API.
Anti-patterns
NEVER do these. Each is explained with the correct fix in references/anti-patterns.md.
- Exposing
anyhow::Errorin a library's public API. Callers cannot match on the failure, only print it. Libraries MUST return athiserrorenum. - Hand-rolling a
thiserrorenum for a binary's top-level error. When no caller ever matches the variants, this is boilerplate with zero benefit; useanyhow. - Losing the source chain. Formatting an inner error into the message string instead of marking the field
#[source]or#[from]makesError::source()dead-end. .unwrap()on ananyhow::Result. It panics and discards the context chain. Propagate with?plus.context(...).- Deriving
thiserror::ErrorwithoutDebug.std::error::ErrorrequiresDebug;#[derive(Error)]alone fails to compile. - Swallowing context by mapping to
String..map_err(|e| e.to_string())?flattens a rich error into text and severssource(). Keep the typed error or wrap withanyhow.
Reference files
references/methods.md: everythiserrorattribute andanyhowmacro/method with signatures and behavior.references/examples.md: complete enum patterns, the combined library-plus-app layout, and migration before/after.references/anti-patterns.md: the six anti-patterns above, each with the failing code and the correct fix.
Verified sources
- thiserror: https://docs.rs/thiserror/latest/thiserror/ (verified 2026-05-20, v2.x)
- anyhow: https://docs.rs/anyhow/latest/anyhow/ (verified 2026-05-20, v1.x)
- The Rust Book, ch. 9 Error Handling: https://doc.rust-lang.org/book/ch09-00-error-handling.html
- Rust API Guidelines (error types): https://rust-lang.github.io/api-guidelines/