From e58614de2edc935891588df36b3d2f299f5a527d Mon Sep 17 00:00:00 2001 From: Rodin Date: Thu, 30 Apr 2026 14:53:12 -0700 Subject: [PATCH] docs: error handling patterns from rust-lang/rust 10 patterns, 702 lines. Full new-spec compliance: - Source hyperlinks (commit SHA permalinks) - Before/after code transformation pairs - Over-application examples with alternatives - Anti-patterns with DON'T/DO blocks - Decision tree at end - Cross-references Patterns: Result, ? operator, error enums, From impls, Error trait, type aliases, panic!, map_err, #[must_use], unwrap/expect. --- patterns/error-handling.md | 702 +++++++++++++++++++++++++++++++++++++ 1 file changed, 702 insertions(+) create mode 100644 patterns/error-handling.md diff --git a/patterns/error-handling.md b/patterns/error-handling.md new file mode 100644 index 0000000..bf39ffc --- /dev/null +++ b/patterns/error-handling.md @@ -0,0 +1,702 @@ +# Rust Error Handling Patterns + +Patterns for error handling in Rust, extracted from the standard +library source at rust-lang/rust. + +**Source:** [rust-lang/rust](https://github.com/rust-lang/rust) at commit +[`f53b654`](https://github.com/rust-lang/rust/tree/f53b654a8882fd5fc036c4ca7a4ff41ce32497a6) + +**Stats:** 4,702 `?` operator usages, 4,059 `Result<>` types, 347 Error +implementations, 18 error enums, 508 `From<>` impls in library/. + +--- + +## 1. Result as the Primary Error Vehicle + +### Source: + +[library/core/src/result.rs#L557](https://github.com/rust-lang/rust/blob/f53b654a8882fd5fc036c4ca7a4ff41ce32497a6/library/core/src/result.rs#L557) + +```rust +// library/core/src/result.rs:557 +#[must_use = "this `Result` may be an `Err` variant, which should be handled"] +pub enum Result { + Ok(T), + Err(E), +} +``` + +### Why + +The `#[must_use]` annotation means the compiler WARNS if you ignore +a Result. This is Rust's core error philosophy: errors are values in +the type system, not exceptions you can accidentally miss. The compiler +is your error-handling auditor. + +### When to Use + +**Triggers:** +- Any operation that can fail for reasons outside the caller's control +- I/O, parsing, network, file operations +- Anything where the caller should decide how to handle failure + +**Example — before:** +```rust +// C-style: return codes that can be ignored +fn parse_port(s: &str) -> i32 { + match s.parse::() { + Ok(n) => n, + Err(_) => -1, // magic sentinel value — caller might not check + } +} + +let port = parse_port("abc"); +// Caller happily uses -1 without knowing it's an error +``` + +**Example — after:** +```rust +fn parse_port(s: &str) -> Result { + s.parse::() +} + +let port = parse_port("abc"); +// Compiler warning if you don't handle this Result +// You MUST match, unwrap, or propagate with ? +``` + +### When NOT to Use + +**Don't use this when:** +- The operation genuinely cannot fail (use plain return type) +- Failure represents a programming bug, not a recoverable condition + (use `panic!` — see pattern #7) +- You're in a context where you can't propagate errors (e.g., trait + implementations with fixed signatures) + +**Over-application example:** +```rust +// Unnecessary: Vec::push cannot fail (barring OOM, which is a panic) +fn add_item(items: &mut Vec, item: String) -> Result<(), ()> { + items.push(item); + Ok(()) // This Result is meaningless — push always succeeds +} +``` + +**Better alternative:** +```rust +fn add_item(items: &mut Vec, item: String) { + items.push(item); +} +``` + +--- + +## 2. The ? Operator (Early Return on Error) + +### Source: + +[library/core/src/ops/try_trait.rs](https://github.com/rust-lang/rust/blob/f53b654a8882fd5fc036c4ca7a4ff41ce32497a6/library/core/src/ops/try_trait.rs) (Try trait definition) + +```rust +// The ? operator desugars to approximately: +match expression { + Ok(val) => val, + Err(err) => return Err(From::from(err)), +} +``` + +4,702 usages of `?` in the stdlib library/ directory. It's THE +idiomatic way to propagate errors. + +### Why + +Eliminates nested match statements for error propagation. Makes the +"happy path" readable as sequential code while still handling every +error. The `From::from(err)` call enables automatic error type +conversion. + +### When to Use + +**Triggers:** +- You're calling a function that returns Result and want to propagate + its error to your caller +- You have multiple fallible operations in sequence +- Your function's error type can be converted From the callee's error + +**Example — before:** +```rust +fn read_config(path: &str) -> Result { + let contents = match std::fs::read_to_string(path) { + Ok(s) => s, + Err(e) => return Err(MyError::Io(e)), + }; + let config = match toml::from_str(&contents) { + Ok(c) => c, + Err(e) => return Err(MyError::Parse(e)), + }; + Ok(config) +} +``` + +**Example — after:** +```rust +fn read_config(path: &str) -> Result { + let contents = std::fs::read_to_string(path)?; // auto-converts via From + let config = toml::from_str(&contents)?; + Ok(config) +} +``` + +### When NOT to Use + +**Don't use this when:** +- You need to handle the error differently depending on its type + (match on it instead) +- You want to add context before propagating (use `.map_err()` first) +- You're in a function that doesn't return Result (use match or + `if let`) + +**Over-application example:** +```rust +// Bad: ? discards context about WHERE the error happened +fn process(data: &[u8]) -> Result<(), io::Error> { + validate(data)?; // if this fails, caller has no idea which step failed + transform(data)?; // same error type, same message — which one broke? + persist(data)?; +} +``` + +**Better alternative:** +```rust +fn process(data: &[u8]) -> Result<(), ProcessError> { + validate(data).map_err(|e| ProcessError::Validation(e))?; + transform(data).map_err(|e| ProcessError::Transform(e))?; + persist(data).map_err(|e| ProcessError::Persist(e))?; + Ok(()) +} +``` + +--- + +## 3. Error Enums for Domain Errors + +### Source: + +[library/std/src/io/error.rs](https://github.com/rust-lang/rust/blob/f53b654a8882fd5fc036c4ca7a4ff41ce32497a6/library/std/src/io/error.rs) (ErrorKind enum) + +```rust +// library/core/src/io/error/kind.rs (ErrorKind variants) +#[non_exhaustive] +pub enum ErrorKind { + NotFound, + PermissionDenied, + ConnectionRefused, + ConnectionReset, + ConnectionAborted, + NotConnected, + AddrInUse, + // ... 30+ variants +} +``` + +### Why + +Enums make error matching exhaustive — the compiler tells you when +you miss a case. `#[non_exhaustive]` allows adding variants without +breaking downstream code. 18 public error enums in the stdlib. + +### When to Use + +**Triggers:** +- Your module has 3+ distinct failure modes +- Callers need to handle different failures differently +- You want compile-time exhaustiveness checking + +**Example — before:** +```rust +// Stringly-typed errors — no compile-time checking +fn connect(addr: &str) -> Result { + if !valid_addr(addr) { + return Err("invalid address".to_string()); + } + // Caller does: if err.contains("timeout") { ... } — fragile +} +``` + +**Example — after:** +```rust +#[derive(Debug)] +pub enum ConnectError { + InvalidAddress(String), + Timeout(Duration), + Refused, + TlsFailure(tls::Error), +} + +impl fmt::Display for ConnectError { ... } +impl std::error::Error for ConnectError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + Self::TlsFailure(e) => Some(e), + _ => None, + } + } +} +``` + +### When NOT to Use + +**Don't use this when:** +- You have only one error condition (just use a simple struct) +- The error is opaque — callers don't match on it (use a struct with + a kind field, like `io::Error`) +- You're writing application code, not a library (consider `anyhow`) + +--- + +## 4. impl From for MyError (Error Conversion) + +### Source: + +[library/core/src/convert/mod.rs#L460](https://github.com/rust-lang/rust/blob/f53b654a8882fd5fc036c4ca7a4ff41ce32497a6/library/core/src/convert/mod.rs#L460) + +```rust +// The stdlib demonstrates the pattern in documentation: +// library/core/src/convert/mod.rs:557 +impl From for CliError { + fn from(error: io::Error) -> Self { + CliError::Io(error) + } +} +impl From for CliError { + fn from(error: num::ParseIntError) -> Self { + CliError::Parse(error) + } +} +``` + +508 `From<>` implementations in the stdlib library/. + +### Why + +Makes the `?` operator work across error types. When you write +`file.read()?` in a function returning `Result<_, MyError>`, the +compiler calls `MyError::from(io_error)` automatically. This is +why `?` can convert errors — it's powered by `From`. + +### When to Use + +**Triggers:** +- Your error enum wraps errors from multiple dependencies +- You want `?` to work without explicit `.map_err()` +- The conversion is infallible and lossless + +**Example — before:** +```rust +fn load(path: &str) -> Result { + let text = std::fs::read_to_string(path) + .map_err(|e| AppError::Io(e))?; // manual conversion everywhere + let data = serde_json::from_str(&text) + .map_err(|e| AppError::Json(e))?; // repetitive + Ok(data) +} +``` + +**Example — after:** +```rust +impl From for AppError { + fn from(e: io::Error) -> Self { AppError::Io(e) } +} +impl From for AppError { + fn from(e: serde_json::Error) -> Self { AppError::Json(e) } +} + +fn load(path: &str) -> Result { + let text = std::fs::read_to_string(path)?; // From converts automatically + let data = serde_json::from_str(&text)?; // clean, linear code + Ok(data) +} +``` + +### When NOT to Use + +**Don't use this when:** +- You need to add context during conversion (use `.map_err()`) +- The conversion is lossy (implement `TryFrom` instead) +- Multiple source types would conflict (can only have one + `From` impl per target type) + +--- + +## 5. The Error Trait (Display + Debug + source) + +### Source: + +[library/core/src/error.rs#L57](https://github.com/rust-lang/rust/blob/f53b654a8882fd5fc036c4ca7a4ff41ce32497a6/library/core/src/error.rs#L57) + +```rust +// library/core/src/error.rs +pub trait Error: Debug + Display { + fn source(&self) -> Option<&(dyn Error + 'static)> { + None + } +} +``` + +### Why + +The trait requires `Debug + Display` — errors must always be +printable. `source()` creates an error chain: each error can point +to its cause, enabling "error: caused by: caused by:" output. + +The doc comment mandates: "Error messages are typically concise +lowercase sentences without trailing punctuation." + +### When to Use + +**Triggers:** +- Any type you intend to use as `E` in `Result` +- Any type that will be stored in `Box` +- When your error wraps a lower-level cause + +**Example — before:** +```rust +// Just derive Debug and call it a day — loses Display and source chain +#[derive(Debug)] +struct MyError { msg: String } + +// Can't use with `Box`, can't chain, can't Display +``` + +**Example — after:** +```rust +#[derive(Debug)] +struct ReadConfigError { + path: PathBuf, + source: io::Error, +} + +impl fmt::Display for ReadConfigError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "unable to read configuration at {}", self.path.display()) + } +} + +impl Error for ReadConfigError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + Some(&self.source) + } +} +``` + +### When NOT to Use + +**Don't use this when:** +- You're writing a quick script (just use `String` or `anyhow`) +- The error will never cross an API boundary + +--- + +## 6. Type Aliases for Result (io::Result, fmt::Result) + +### Source: + +[library/std/src/io/error.rs](https://github.com/rust-lang/rust/blob/f53b654a8882fd5fc036c4ca7a4ff41ce32497a6/library/std/src/io/error.rs) + +```rust +// library/std/src/io/mod.rs +pub type Result = result::Result; +``` + +### Why + +Reduces boilerplate in modules where one error type dominates. +Instead of writing `Result, io::Error>` everywhere, you +write `io::Result>`. Every function in `std::io` uses this. + +### When to Use + +**Triggers:** +- Your module has many functions that all return the same error type +- The convention is clear and well-documented (module-scoped) + +**Example — before:** +```rust +pub fn read(path: &Path) -> Result, io::Error> { ... } +pub fn write(path: &Path, data: &[u8]) -> Result<(), io::Error> { ... } +pub fn copy(from: &Path, to: &Path) -> Result { ... } +``` + +**Example — after:** +```rust +pub type Result = std::result::Result; + +pub fn read(path: &Path) -> Result> { ... } +pub fn write(path: &Path, data: &[u8]) -> Result<()> { ... } +pub fn copy(from: &Path, to: &Path) -> Result { ... } +``` + +### When NOT to Use + +**Don't use this when:** +- Functions in your module return different error types +- The alias would shadow `std::result::Result` confusingly +- You're writing application code (just use the full type) + +--- + +## 7. panic! for Unrecoverable Bugs + +### Source: + +[library/core/src/macros/mod.rs](https://github.com/rust-lang/rust/blob/f53b654a8882fd5fc036c4ca7a4ff41ce32497a6/library/core/src/macros/mod.rs) (panic! macro) + +514 `panic!` calls in library/ (non-test). + +```rust +// Common patterns: +panic!("index out of bounds: the len is {len} but the index is {index}"); +panic!("internal error: entered unreachable code"); +``` + +### Why + +Panics are for programmer errors — bugs that should never happen in +correct code. They unwind the stack (or abort). The distinction: +**Result = expected failure, panic! = bug.** + +### When to Use + +**Triggers:** +- Invariant violation (should be impossible if the code is correct) +- Index out of bounds on internal data structures +- Unrecoverable state corruption +- `unreachable!()` branches + +**Example — before:** +```rust +// Returning errors for things that are actually bugs: +fn get_cached(&self, key: &str) -> Result<&Value, CacheError> { + self.cache.get(key).ok_or(CacheError::NotFound) + // If this key was SUPPOSED to be there, this is a bug, not an error +} +``` + +**Example — after:** +```rust +fn get_cached(&self, key: &str) -> &Value { + self.cache.get(key) + .expect("cache invariant violated: key must exist after initialization") + // Panic with a useful message — this is a bug in OUR code +} +``` + +### When NOT to Use + +**Don't use this when:** +- The failure can be caused by user input (use Result) +- The failure can be caused by external systems (use Result) +- You're writing a library (let the caller decide how to handle it) +- Recovery is possible and meaningful + +### Anti-pattern + +```rust +// DON'T: panic on user-facing errors +fn parse_config(input: &str) -> Config { + toml::from_str(input).unwrap() // panics on invalid TOML — terrible UX +} + +// DO: return Result for anything that depends on external input +fn parse_config(input: &str) -> Result { + toml::from_str(input) +} +``` + +--- + +## 8. .map_err() for Adding Context + +### Source: + +[library/core/src/result.rs](https://github.com/rust-lang/rust/blob/f53b654a8882fd5fc036c4ca7a4ff41ce32497a6/library/core/src/result.rs) + +```rust +// library/core/src/result.rs +pub fn map_err F>(self, op: O) -> Result { + match self { + Ok(t) => Ok(t), + Err(e) => Err(op(e)), + } +} +``` + +### Why + +When `?` alone doesn't provide enough context — you know WHAT failed +but not WHERE or WHY — `.map_err()` lets you add information before +propagating. + +### When to Use + +**Triggers:** +- The error type needs context (which file? which step?) +- You can't implement `From` because the same source error type + appears in multiple places with different meanings +- You want custom error messages per call site + +**Example — before:** +```rust +fn load_users(path: &Path) -> Result, AppError> { + let data = std::fs::read_to_string(path)?; // error: "No such file" + // But WHICH file? The caller has no idea what path was attempted. +} +``` + +**Example — after:** +```rust +fn load_users(path: &Path) -> Result, AppError> { + let data = std::fs::read_to_string(path) + .map_err(|e| AppError::FileRead { path: path.to_owned(), source: e })?; + // error: "failed to read users file at /etc/users.json: No such file" +} +``` + +### When NOT to Use + +**Don't use this when:** +- `From` conversion provides enough context +- You're adding noise ("error: an error occurred" — not helpful) +- The caller already knows the context + +--- + +## 9. #[must_use] on Result-Returning Functions + +### Source: + +[library/core/src/result.rs#L557](https://github.com/rust-lang/rust/blob/f53b654a8882fd5fc036c4ca7a4ff41ce32497a6/library/core/src/result.rs#L557) + +```rust +#[must_use = "this `Result` may be an `Err` variant, which should be handled"] +pub enum Result { ... } +``` + +1,963 `#[must_use]` annotations across the stdlib. + +### Why + +The compiler warns when a `#[must_use]` value is discarded. Since +Result itself is `#[must_use]`, every function returning Result +automatically gets this protection. Additional `#[must_use]` on +functions adds a custom warning message. + +### When to Use + +**Triggers:** +- Functions where ignoring the return value is always a bug +- Builder methods that return a new value (not &mut self) +- Functions with side effects where the Result indicates success + +### When NOT to Use + +**Don't use this when:** +- The function is called primarily for side effects and the return + value is informational (e.g., `write!` in logging — you might + legitimately not care) + +--- + +## 10. unwrap()/expect() — Only in Tests and Invariants + +### Source: + +1,790 `unwrap()`/`expect()` calls in library/ (non-test). These +are concentrated in: +- Internal invariant checks (the value MUST be Some/Ok by construction) +- Examples in documentation +- Early initialization code + +```rust +// Acceptable: known-good initialization +let regex = Regex::new(r"^\d+$").unwrap(); // compile-time-known pattern + +// Acceptable: documented invariant +fn pop_front(&mut self) -> T { + self.queue.pop_front().expect("pop_front called on empty queue") +} +``` + +### Why + +`expect()` is preferred over `unwrap()` because the message documents +WHY the value should be present. It's a comment that also appears in +the panic output. + +### When to Use + +**Triggers:** +- The value is guaranteed present by prior logic (invariant) +- You're in test code (tests should panic on unexpected errors) +- The error condition is literally impossible (compile-time constant) + +### When NOT to Use + +**Don't use this when:** +- The value might legitimately be None/Err at runtime +- You're writing library code that callers use in production +- There's a reasonable recovery path + +### Anti-pattern + +```rust +// DON'T: unwrap on user input +let port: u16 = args.get("port").unwrap().parse().unwrap(); + +// DO: propagate errors to the caller +let port: u16 = args.get("port") + .ok_or_else(|| ConfigError::Missing("port"))? + .parse() + .map_err(|e| ConfigError::Invalid("port", e))?; +``` + +--- + +## Summary: Error Handling Decision Tree + +``` +Can this operation fail? +├── NO → Return plain value (no Result) +├── YES → Is failure a BUG or an expected condition? +│ ├── BUG (invariant violation) → panic!/expect() +│ └── EXPECTED → Return Result +│ ├── What should E be? +│ │ ├── Single module, one error type → type alias (io::Result) +│ │ ├── Multiple failure modes → Error enum +│ │ ├── Wraps lower-level errors → impl From for Mine +│ │ └── Quick prototype → Box or anyhow::Error +│ └── How to propagate? +│ ├── Same error type → ? operator +│ ├── Need conversion → ? with From impl +│ └── Need context → .map_err() then ? +``` + +| When you need to... | Use | +|---|---| +| Signal possible failure | `Result` | +| Propagate errors up | `?` operator | +| Convert error types for `?` | `impl From for Target` | +| Add context to an error | `.map_err(\|e\| ...)` | +| Match on error variants | `enum MyError { ... }` | +| Chain error causes | `Error::source()` | +| Reduce `Result` boilerplate | `type Result = std::result::Result` | +| Fail on programmer bugs | `panic!()` / `expect()` | +| Ignore errors (test/example code only) | `.unwrap()` | + +See also: +- [traits.md](traits.md) — From/Into trait design +- [api-design.md](api-design.md) — Public API error conventions +- [documentation.md](documentation.md) — `# Errors` doc sections + +