Rust error handling is an architecture for recovery, not syntax

Architecture & systems SeedlingPlanted Sep 2026

Rust error handling is an architecture for recovery, not syntax. Choosing an enum, adding context, using ?, or converting into a boxed error determines which failures callers can distinguish, which causes remain inspectable, and where the system is allowed to recover. The code may occupy a few lines, but it defines the failure protocol between components.

Result makes fallibility part of the function’s type, and #[must_use] prevents it from disappearing quietly. That is already an architectural commitment: success and failure are both expected control paths. The question-mark operator does not remove that obligation; it delegates it through a conversion boundary. Every From implementation decides whether upstream distinctions survive, collapse, or leak into a public API. Convenient propagation can therefore preserve structure—or erase the exact state needed for recovery.

I prefer typed errors in libraries because the caller owns policy. A storage layer may distinguish unavailable, conflict, corrupt data, and invalid key. The application can then retry availability failures, reconcile conflicts, quarantine corruption, and reject invalid input. If the library returns one narrative string, it has forced every caller into parsing prose or treating all failures alike. thiserror helps express the contract, but the important decision is the enum’s recovery semantics, not the derive macro.

Application boundaries want a different shape. Once the program owns the recovery decision and needs to report an incident, contextual wrappers such as anyhow or eyre can add the operation, resource, and user-visible consequence while retaining the source chain. The layered pattern—typed errors below, contextual reports above—separates machine branching from human diagnosis. It mirrors why tool errors must be typed for agents: probabilistic and deterministic callers both need stable categories before they can choose a safe next transition.

Panics mark another architectural boundary. An invalid external value, missing file, or network timeout is not a broken invariant and should not terminate the process. A violated internal invariant may justify a panic, but the runtime still needs a containment decision: unwind to run destructors, abort for a smaller and simpler failure path, or catch unwinding only across a plugin or foreign-function boundary designed as a sandbox. Treating unwrap as a stylistic lint misses the real question—whether the failure is recoverable here and what state remains valid afterward.

Distributed and asynchronous code sharpens the issue. A task failure may mean cancellation, panic, timeout, or domain rejection; joining those into one opaque error destroys the scheduler’s options. Error types should preserve effect status and retryability, while tracing or virtual stack context explains the path across await boundaries. Reliability begins with named failure modes, and supervision trees contain failure only when children report enough structure for the supervisor to restart, stop, or escalate correctly.

There is one precise concession: a small command-line application with one owner may reasonably use a single contextual error type throughout. The caller and operator are effectively the same person, and there may be no recovery branch beyond printing a useful report and exiting. The boundary changes when code becomes a library, service, plugin host, long-running process, or cross-language API.

I review Rust errors by asking what each boundary permits next. Can the caller distinguish correction from retry, uncertainty from confirmed failure, and local damage from invalid process state? Does context accumulate without discarding causes? Are public variants stable enough to be a contract? Good Rust error handling does more than make failure explicit. It makes recovery responsibilities explicit—and that is architecture.