This is a searchable reference for Rust error handling — the std::error::Error trait, the Result/Option enums, the io::ErrorKind variants, the standard parse error types (ParseIntError, Utf8Error), and the conventions of the widely used anyhow and thiserror crates.
How it works
Rust has no exceptions. Fallible operations return Result<T, E> (either Ok(T) or Err(E)), and absence is modelled with Option<T>. The std::error::Error trait unifies error values and exposes source() to walk a cause chain. Concrete errors like io::Error carry an ErrorKind enum — NotFound, PermissionDenied, TimedOut — so you can match on the category portably with err.kind() instead of inspecting raw OS numbers.
You propagate failures with the ? operator. Applied to a Result, it returns early on Err, automatically calling From to convert that error into the function’s declared error type. Libraries typically define their own error enum (often derived with thiserror and #[from] for those conversions), while applications box everything behind anyhow::Error and attach human-readable context().
Example
Parse a number, propagate the error, then inspect an I/O kind:
use std::io::{self, ErrorKind};
fn read_port(s: &str) -> Result<u16, Box<dyn std::error::Error>> {
let n: u16 = s.trim().parse()?; // ? converts ParseIntError via From
Ok(n)
}
match std::fs::read("config.toml") {
Ok(bytes) => { /* ... */ }
Err(e) if e.kind() == ErrorKind::NotFound => { /* use defaults */ }
Err(e) => return Err(e.into()),
}
Reach for unwrap() only when failure is truly impossible — otherwise prefer ?, which keeps the program recoverable instead of panicking. Everything runs in your browser; nothing is uploaded.
Choosing the right error strategy
Rust’s ecosystem has converged on clear conventions, and knowing which approach belongs where prevents the common mistake of re-designing an error system mid-project.
In a library
Define your own error enum. Use thiserror to avoid the boilerplate:
use thiserror::Error;
#[derive(Debug, Error)]
pub enum MyError {
#[error("IO failure: {0}")]
Io(#[from] std::io::Error),
#[error("parse error: {0}")]
Parse(#[from] std::num::ParseIntError),
#[error("invalid input: {0}")]
Invalid(String),
}
The #[from] attribute auto-generates From implementations so ? converts each sub-error into MyError at the call site. Callers can match on specific variants.
In a binary or application
Use anyhow. Return anyhow::Result<T> and add context with .context() or .with_context():
use anyhow::{Context, Result};
fn read_config(path: &str) -> Result<Config> {
let raw = std::fs::read_to_string(path)
.with_context(|| format!("failed to read config from {path}"))?;
serde_json::from_str(&raw).context("config file is invalid JSON")
}
The resulting error messages chain cleanly: “config file is invalid JSON: expected : at line 3 column 5”.
The io::ErrorKind matrix
Matching on err.kind() is the portable, OS-independent way to handle I/O errors:
| ErrorKind | Common cause | Typical action |
|---|---|---|
NotFound | Missing file or directory | Create default, or prompt user |
PermissionDenied | OS access restriction | Log, escalate, or fail gracefully |
AlreadyExists | Creating a file that exists | Use create_new flag or skip |
TimedOut | Network or I/O timeout | Retry with backoff |
ConnectionRefused | Remote not listening | Check service availability |
InvalidData | Malformed bytes | Return parse error to caller |
UnexpectedEof | Stream cut short | Treat as partial data error |
Common pitfalls
.unwrap()in production code is a panic waiting to happen. Reserve it for prototypes or genuinely impossible cases; use.expect("reason")at minimum so the panic message explains why failure should be impossible.- Mixing
Box<dyn Error>and concrete error types in the same function makes theFromchain hard to reason about. Pick one style per function signature. - Ignoring the cause chain:
anyhow::Errorstores source errors; always use{:#}(the alternate debug format) when printing, as it formats the full chain rather than just the outermost message.