Rust has no exceptions; errors are ordinary values returned as Result<T, E>
or Option<T>. The ecosystem has settled on a small set of idioms for
propagating and converting them. This tool is a reference of those patterns,
each with a worked snippet and a note on when it is the right choice.
The mental model: errors are just values
In most languages, errors are thrown out-of-band and caught in a separate control flow path. In Rust, a function that can fail returns Result<T, E>, which is just an enum — either Ok(T) (the success value) or Err(E) (the error value). The caller handles it like any other value. This means the compiler enforces error handling: you cannot accidentally ignore a Result (the #[must_use] attribute makes unused Results a warning), and you cannot call methods on a value that might be absent without first checking it.
The tradeoff is that you must decide, at every fallible call site, what to do with a failure. The idioms below are how the community handles that decision cleanly.
How it works
Result<T, E> represents success (Ok) or failure (Err); Option<T>
represents presence (Some) or absence (None). The patterns below combine and
convert these:
- Propagate with
?— the workhorse; bubbles errors up, converting viaFrom. - Extract or crash with
unwrap/expect— fine for tests and proven invariants, dangerous in production. - Transform with
map_err,ok_or,unwrap_or,unwrap_or_else— adjust the error or supply a fallback. - Define custom error enums, generated cheaply with
thiserror. - Box disparate errors with
anyhowfor application code.
Worked example
use std::fs;
use thiserror::Error;
#[derive(Error, Debug)]
enum ConfigError {
#[error("could not read config: {0}")]
Io(#[from] std::io::Error), // #[from] enables ? to convert io::Error
#[error("empty config file")]
Empty,
}
fn load(path: &str) -> Result<String, ConfigError> {
let text = fs::read_to_string(path)?; // io::Error -> ConfigError::Io
if text.is_empty() {
return Err(ConfigError::Empty);
}
Ok(text)
}
For an application binary you might instead use anyhow:
use anyhow::{Context, Result};
fn load(path: &str) -> Result<String> {
let text = std::fs::read_to_string(path)
.with_context(|| format!("reading {path}"))?;
Ok(text)
}
Quick decision guide
| Situation | Reach for |
|---|---|
| Simple propagation through a function | ? operator |
| Value must exist or it’s a bug | expect("explain the invariant") |
| Convert error type before propagating | map_err(MyError::from) or #[from] |
| Provide a default on None/Err | unwrap_or(default) or unwrap_or_default() |
| Building a library with typed errors | thiserror + custom enum |
| Building an application binary | anyhow |
| Lazy error production (expensive message) | unwrap_or_else(|_| compute()) |
Notes
- The
#[from]attribute on athiserrorvariant generates aFromimpl so?converts the source error automatically — no manualmap_errneeded. - Reserve
unwrapfor cases where a failure is genuinely a bug; preferexpect("reason")so the panic message names the broken invariant. unwrap_or_default()is a clean fallback when the type implementsDefault.- Use
thiserrorin libraries (typed, matchable) andanyhowin applications (boxed, contextual) — mixing them is common and intentional. ok_orandok_or_elseconvertOption<T>intoResult<T, E>so you can use?on an option.- Avoid
Box<dyn Error>in library return types — callers cannot match on it. Usethiserrorenums instead and let application code box them withanyhowif needed.