Error Propagation, Context, and Messages
Rule
Propagate with ?, preserve source chains, add concise context at operation boundaries, and never stringify a source merely to add context.
Why
Useful chains explain both the failed operation and its cause. Context on every line is repetitive; flattening to text destroys structure.
Do
- Use
?for ordinary propagation. - Use
Fromor#[from]for conversions that need no additional fields. - Use
.context(...)for static application context and.with_context(...)when formatting values. - Add context at command, request, job, service, task, crate, or layer boundaries.
- Keep typed error messages specific to their local failure.
- Preserve sources with
#[source],#[from],anyhow::Context, or explicit fields. - Render the full chain deliberately at the outer boundary.
Avoid
- Do not annotate every
?. - Do not restate the lower-level message in context.
- Do not use
.map_err(|err| err.to_string())oranyhow!("{err}")for propagation. - Do not interpolate a source error into a replacement context string.
- Do not put secrets or unredacted request data in messages.
Library vs Application
Libraries describe local failures with typed errors. Applications add operation context and choose final CLI, API, worker, or log rendering.
Example
#![allow(unused)]
fn main() {
use anyhow::{Context as _, Result};
fn run(path: &Path) -> Result<()> {
let config = config_lib::load(path)
.with_context(|| format!("loading configuration from {}", path.display()))?;
start_server(config).context("starting server")
}
}
Avoid flattening the chain:
#![allow(unused)]
fn main() {
let config = config_lib::load(path)
.map_err(|err| anyhow::anyhow!("failed to load config: {err}"))?;
}
Exceptions
- Add context near a call when no higher boundary can identify the operation.
- Keep propagation sparse when typed variants already explain the operation.