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.
This commit is contained in:
Rodin
2026-04-30 14:53:12 -07:00
parent 4de77cf306
commit e58614de2e
+702
View File
@@ -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<T, E> 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<T, E> {
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::<i32>() {
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<u16, std::num::ParseIntError> {
s.parse::<u16>()
}
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<String>, item: String) -> Result<(), ()> {
items.push(item);
Ok(()) // This Result is meaningless — push always succeeds
}
```
**Better alternative:**
```rust
fn add_item(items: &mut Vec<String>, 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<Config, MyError> {
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<Config, MyError> {
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<Connection, String> {
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<E> 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<io::Error> for CliError {
fn from(error: io::Error) -> Self {
CliError::Io(error)
}
}
impl From<num::ParseIntError> 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<Data, AppError> {
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<io::Error> for AppError {
fn from(e: io::Error) -> Self { AppError::Io(e) }
}
impl From<serde_json::Error> for AppError {
fn from(e: serde_json::Error) -> Self { AppError::Json(e) }
}
fn load(path: &str) -> Result<Data, AppError> {
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<T>` 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<T, E>`
- Any type that will be stored in `Box<dyn Error>`
- 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<dyn Error>`, 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<T> = result::Result<T, Error>;
```
### Why
Reduces boilerplate in modules where one error type dominates.
Instead of writing `Result<Vec<u8>, io::Error>` everywhere, you
write `io::Result<Vec<u8>>`. 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<Vec<u8>, io::Error> { ... }
pub fn write(path: &Path, data: &[u8]) -> Result<(), io::Error> { ... }
pub fn copy(from: &Path, to: &Path) -> Result<u64, io::Error> { ... }
```
**Example — after:**
```rust
pub type Result<T> = std::result::Result<T, Error>;
pub fn read(path: &Path) -> Result<Vec<u8>> { ... }
pub fn write(path: &Path, data: &[u8]) -> Result<()> { ... }
pub fn copy(from: &Path, to: &Path) -> Result<u64> { ... }
```
### 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<Config, toml::de::Error> {
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, O: FnOnce(E) -> F>(self, op: O) -> Result<T, F> {
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<Vec<User>, 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<Vec<User>, 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<T, E> { ... }
```
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<T, E>
│ ├── What should E be?
│ │ ├── Single module, one error type → type alias (io::Result)
│ │ ├── Multiple failure modes → Error enum
│ │ ├── Wraps lower-level errors → impl From<Lower> for Mine
│ │ └── Quick prototype → Box<dyn Error> 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<T, E>` |
| Propagate errors up | `?` operator |
| Convert error types for `?` | `impl From<Source> for Target` |
| Add context to an error | `.map_err(\|e\| ...)` |
| Match on error variants | `enum MyError { ... }` |
| Chain error causes | `Error::source()` |
| Reduce `Result<T, MyError>` boilerplate | `type Result<T> = std::result::Result<T, MyError>` |
| 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
<!-- PATTERN_COMPLETE -->