.. /index

Rust Error Handling

Rust Error Handling

The Rust error-handling ecosystem has converged into four complementary layers rather than a single "best crate." A key enabler: Error moved to core in Rust 1.81 (September 2024), enabling no_std error types without external crates — see modern-rust-features. The fundamental organizing principle is a two-layer architecture: typed errors for libraries, type-erased reports for applications. Within each layer, the key strategic choice is string context (fast, flexible, opaque) vs structured context (more modeling upfront, better debugging at scale). See rust-error-crate-comparison for the detailed comparative analysis.

The underlying algebra — Result<T, E> as a sum type with monadic composition — is now the cross-language consensus. C++23's std::expected<T, E> is the direct mirror, with .and_then() / .transform() / .or_else() paralleling Rust's combinators almost verbatim. Both languages adopted the same railway-oriented-programming discipline at roughly the same time, and the same open problem — how to attach context across a pipeline so the final error carries a useful trail — applies to both. Rust's answer is the four-layer architecture below; C++ has not yet settled on an equivalent and its ecosystem is the open frontier as of 2026.

Layer 1: Typed error definition (libraries)

Neither thiserror nor snafu has meaningful runtime overhead. The choice is purely API design philosophy: familiarity and conciseness (thiserror) vs enforced discipline and structured context (snafu).

Layer 2: Type-erased reporting (applications)

Layer 3: Diagnostic rendering (parsers, compilers, CLI UX)

Layer 4: Observability context (async services)

Legacy crates to avoid

Recommendation

When to panic vs return Result

Return Result as the default. Panic only for contract violations (bugs in the caller, not expected conditions), in tests, and for logically infallible operations like "127.0.0.1".parse::<IpAddr>().expect("hardcoded IP"). Never panic in Drop — if drop() panics during stack unwinding from another panic, the program aborts.

Error formatting conventions

RisingWave's production guidelines prevent error message chaos in large codebases: display messages should be lowercase, no trailing punctuation, describing only themselves — never embedding their source. Error reports walk the .source() chain to compose full messages. Each layer adds only its own context, preventing the anti-pattern of format!("failed to do X: {}", source) which duplicates information when the chain is printed.

Recommendation

For most projects: thiserror at library boundaries, anyhow for application plumbing. Switch to snafu in large workspaces where error discipline matters. Add miette for compiler-style UX, tracing-error for async observability, or error-stack when you need structured attachments on the error path. See expert-rust-design for how error handling fits into the broader design philosophy.

Linked from

Sources