It also covers the recoverable-errors-vs-panics decision — Result for anything a caller could reasonably recover from, panic!/unwrap/expect reserved for broken invariants — and the rule that anyhow::Error must never leak into a library's public API.
Out of scope: general ownership/borrowing design belongs to rust-core-language; async-runtime-specific error handling is not covered here.
Rust Error Handling
The consensus split: thiserror for libraries, anyhow for applications. A library
exposes a typed error its callers can match; an application wants one ergonomic error
type and rich context. Getting this boundary right is the whole discipline.
Agent Workflow (MANDATORY)
- fuse-ai-pilot:explore-codebase — is this crate a library (published API, other code depends on it) or a binary/application? That answer picks the tool.
- fuse-ai-pilot:research-expert — confirm current
thiserror/anyhowAPI before writing derives (verification chain: fuse-browser fast-path ondocs.rs/thiserroranddocs.rs/anyhow→ Context7 → Exa). - After writing, run fuse-ai-pilot:sniper and
cargo clippy.
The rule
| Crate kind | Tool | Why |
|---|---|---|
| Library (others depend on it) | thiserror |
Typed enum, #[derive(Error)], callers can match on variants |
| Application (binary, top level) | anyhow |
anyhow::Result<T>, ? everywhere, .context() for breadcrumbs |
| Simple / dep-free | hand-written std::error::Error |
No dependency when one or two variants suffice |
Critical Rules
- Never expose
anyhow::Errorin a library's public API. It erases the type, so callers cannot match or handle specific failures. Return a typedenumerror. thiserror is designed to not appear in your public API — switching to/from a hand-written impl is not a breaking change. - Applications use
anyhow; libraries usethiserror. Do not pullanyhowinto a reusable library's signatures. - Add context at each layer in application code:
.context("...")/.with_context(|| ...)turns "No such file or directory" into a traceable chain. #[from]for zero-boilerplate conversion, so?lifts a source error into your enum.#[from]implies#[source]— never write both.- Errors are values; panics are bugs. Use
Resultfor anything a caller could reasonably recover from. Reservepanic!/unwrap/expectfor broken invariants.
Reference Guide
Concepts
| Topic | Reference | Load when |
|---|---|---|
| thiserror (libraries) | thiserror-libraries.md | Building a typed error enum for a library API |
| anyhow (applications) | anyhow-applications.md | Handling errors in a binary / top-level app |
| Error design | error-design.md | Shaping enums, #[from] conversion, recoverable vs panic, the anyhow/thiserror boundary |
Templates
| Template | Load when |
|---|---|
| library-error.md | Need a complete thiserror library error module |
| application-error.md | Need a complete anyhow application entry point with context |
Validation Checklist
- Library errors are a typed
enumderivingthiserror::Error— noanyhowin the public API - Application code returns
anyhow::Result<T>and adds.context(...) -
#[from]used for source conversions; no redundant#[source]alongside it -
?used instead ofunwrap()on fallible values - Panics only guard genuine invariants, with a reason
-
cargo clippyclean, sniper passed