# 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