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:
@@ -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 -->
|
||||
Reference in New Issue
Block a user