TL;DR
Errors that are simply forwarded up the stack lose the contextual signals needed by both automated handlers and humans. The article argues for designing error types around what callers should do (retry, create, abort) and for low-friction context capture instead of relying on deep chains or expensive backtraces.
What happened
The piece critiques common Rust error-handling habits that treat errors as things to propagate unchanged rather than design with intent. It argues that std::error::Error assumes linear chains and cannot represent tree-shaped failures such as multi-field validation or partial-results timeouts. Backtraces are called out as costly and of limited use in async code because they show where an error was created, not the logical request path, and are noisy with machinery frames. Newer facilities like the Provide/Request API add dynamic typed access but introduce runtime unpredictability and optimization challenges. Popular libraries are also examined: thiserror often categorizes by origin rather than by action, and anyhow’s type-erasure encourages omission of contextual .context() calls. The author recommends designing errors for two audiences — machines (flat, actionable kinds and explicit retry status) and humans (automatic, low-cost context capture and tree-like frames) — and sketches a single-error-struct approach combining kind, status, message, and contextual fields.
Why it matters
- Forwarded errors frequently lose the runtime context needed to decide automated recovery steps or to debug incidents quickly.
- Automated systems (retries, circuit breakers) need simple, predictable signals about what to do, not deep variant inspection.
- Developers on-call waste time when logs contain terse, origin-only messages instead of actionable details like request IDs or fields.
- Expensive observability techniques (capturing full backtraces) are not always effective, especially in async code paths.
Key facts
- The std::error::Error trait models errors as a chain with an optional source(), which cannot express tree-shaped sources like multiple validation failures.
- Backtraces capture where an error was created but do not record the logical call path or contextual parameters, and they are costly to collect.
- Async stack traces include many runtime frames (e.g., GenFuture::poll), limiting the usefulness of backtraces for suspended tasks.
- The Provide/Request API (RFCs referenced) allows errors to expose typed context dynamically but can be unpredictable at runtime and may complicate optimization.
- thiserror commonly organizes error enums by origin (which dependency failed) rather than by what the caller should do in response.
- anyhow’s type erasure simplifies propagation but makes adding contextual .context() calls optional, increasing the chance of missing important debug information.
- A suggested pattern is a single library Error struct with fields like kind (action-oriented), message, status (retryability), operation, and context pairs.
- Representing errors as trees of frames and using #[track_caller] can capture low-cost source locations and multiple causes, improving human debuggability.
What to watch next
- Whether the Rust standard error model evolves to better represent tree-shaped failures — not confirmed in the source
- Adoption of actionable error kinds and explicit status fields in libraries and services (replacements for origin-based enums) — not confirmed in the source
- Uptake of lightweight context-capture approaches such as tree frames and #[track_caller] in production codebases — not confirmed in the source
Quick glossary
- Error chain: A linked sequence of errors where each error optionally points to a single underlying source.
- Backtrace: A recorded sequence of stack frames showing where an error was created; useful for origin but limited in async contexts.
- ErrorKind: A classification of an error by the action a caller should take (e.g., NotFound, RateLimited) rather than by its origin.
- Retryability / ErrorStatus: A property indicating whether an operation that failed should be retried, considered permanent, or was already retried.
- track_caller: A Rust attribute that records the source location (file/line) of a function call at low cost, used to capture context without heavy backtraces.
Reader FAQ
Why are forwarded errors a problem?
Forwarded errors often preserve a raw message but discard the contextual information callers or responders need to act or debug effectively.
Should libraries use thiserror and applications use anyhow?
The article calls that a myth; instead it recommends reasoning about the intended consumer and the action you expect the caller to take.
Do backtraces solve observability?
Not fully — they are expensive to capture and can be misleading in async code because they show origin frames, not the logical path or request context.
What minimal change can improve errors for humans?
Automatic, low-friction context capture (for example via #[track_caller] and structured context fields) so developers don't skip adding details.

← ← Stop Forwarding Errors, Start Designing Them Stop Forwarding Errors, Start Designing Them Jan 04 · 11 Min Read It’s 3am. Production is down. You’re staring at a log…
Sources
- Stop Forwarding Errors, Start Designing Them
- Stop Deploying AI. Start Designing Intelligence
- The Intersection of AI and Human Factors: A Story of Error …
- Designing AI for better, not fewer, jobs
Related posts
- Venezuela interim government says it stands united in support of Maduro
- Cold-blooded software: how to build projects that survive long dormancy
- Blocking an age-linked protein restores knee cartilage and averts arthritis