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.
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)
}
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.