- traits.md: iterator.rs anchor L76→L42 (pub const trait Iterator) - traits.md: deref.rs anchor L60→L139 (pub const trait Deref) - error-handling.md: fix variant names CliError::Io→IoError, Parse→ParseError to match actual stdlib doc example at convert/mod.rs:557 - concurrency.md: mutex.rs and rwlock.rs moved to sync/poison/ subtree (3 link updates: mutex×2, rwlock×1)
19 KiB
Rust Error Handling Patterns
Patterns for error handling in Rust, extracted from the standard library source at rust-lang/rust.
Source: rust-lang/rust at commit
f53b654
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
// 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:
// 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:
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:
// 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:
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 (Try trait definition)
// 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:
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:
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:
// 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:
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 (ErrorKind enum)
// 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:
// 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:
#[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
// 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::IoError(error)
}
}
impl From<num::ParseIntError> for CliError {
fn from(error: num::ParseIntError) -> Self {
CliError::ParseError(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:
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:
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
TryFrominstead) - 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
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
EinResult<T, E> - Any type that will be stored in
Box<dyn Error> - When your error wraps a lower-level cause
Example — before:
// 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:
#[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
Stringoranyhow) - The error will never cross an API boundary
6. Type Aliases for Result (io::Result, fmt::Result)
Source:
// 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:
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:
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::Resultconfusingly - You're writing application code (just use the full type)
7. panic! for Unrecoverable Bugs
Source:
library/core/src/macros/mod.rs (panic! macro)
514 panic! calls in library/ (non-test).
// 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:
// 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:
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
// 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
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
Frombecause the same source error type appears in multiple places with different meanings - You want custom error messages per call site
Example — before:
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:
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:
Fromconversion 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
#[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
// 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
// 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 — From/Into trait design
- api-design.md — Public API error conventions
- documentation.md —
# Errorsdoc sections