Rust Error Handling Cheat Sheet
Quick-reference for Result, Option, the ? operator, and error propagation patterns. Each section includes copy-ready snippets with inline output comments.
Result<T, E>
Result is an enum with two variants: Ok(T) for success and Err(E) for failure. Rust has no exceptions -- Result is how you handle errors.
let ok: Result<i32, String> = Ok(42);
let err: Result<i32, String> = Err("something went wrong".into());match "42".parse::<i32>() {
Ok(n) => println!("parsed: {n}"),
Err(e) => println!("error: {e}"),
}fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err("division by zero".into())
} else {
Ok(a / b)
}
}Option<T>
Option represents a value that might be absent: Some(T) or None. Rust has no null -- Option replaces it.
let some: Option<i32> = Some(5);
let none: Option<i32> = None;let v = vec![10, 20, 30];
v.first() // => Some(&10)
v.get(99) // => None
"hello".find('l') // => Some(2)
"hello".find('z') // => Nonematch v.first() {
Some(val) => println!("first: {val}"),
None => println!("empty"),
}The ? Operator
The ? operator unwraps Ok/Some or returns early with Err/None. The function must return a compatible type.
use std::fs;
fn read_config() -> Result<String, std::io::Error> {
let content = fs::read_to_string("config.toml")?; // returns Err on failure
Ok(content.trim().to_string())
}fn first_word(s: &str) -> Option<&str> {
let end = s.find(' ')?; // returns None if no space
Some(&s[..end])
}fn load_port() -> Result<u16, Box<dyn std::error::Error>> {
let text = std::fs::read_to_string("port.txt")?;
let port = text.trim().parse::<u16>()?;
Ok(port)
}Box<dyn Error> accepts any error type, making ? work across different error types.
fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = std::fs::read_to_string("config.toml")?;
println!("{config}");
Ok(())
}unwrap and expect
Both panic on Err/None. Use expect to document the assumption; use ? in production code.
let n: i32 = "42".parse().unwrap(); // => 42
// "abc".parse::<i32>().unwrap(); // PANIClet port: u16 = env::var("PORT")
.expect("PORT must be set")
.parse()
.expect("PORT must be a valid u16");The message should explain the assumption, not the symptom.
// Tests: unwrap is fine -- panic = test failure
#[test]
fn test_parse() {
let result = parse("valid").unwrap();
assert_eq!(result, 42);
}
// Provably safe: expect documents why
let one: u32 = "1".parse().expect("literal '1' is always valid");
// Production: prefer ? to propagate
fn load() -> Result<Config, Error> {
let text = std::fs::read_to_string("config.toml")?;
Ok(parse_config(&text)?)
}Result Combinators
Transform Ok/Err values without match. Chain combinators for clean error handling pipelines.
let result: Result<i32, String> = Ok(5);
let doubled = result.map(|x| x * 2); // => Ok(10)let result = std::fs::read_to_string("file.txt")
.map_err(|e| format!("failed to read: {e}"))?;fn parse_port(s: &str) -> Result<u16, String> {
s.trim()
.parse::<u16>()
.map_err(|e| format!("bad port: {e}"))
}
let port = std::fs::read_to_string("port.txt")
.map_err(|e| format!("read failed: {e}"))
.and_then(|s| parse_port(&s));let port = env::var("PORT")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(8080); // default if any step fails
// unwrap_or_else: lazy default (only computed on Err)
let config = load_config()
.unwrap_or_else(|e| {
eprintln!("warn: {e}");
Config::default()
});Option Combinators
Transform Some/None values. Convert between Option and Result with ok_or.
let opt = Some("hello");
let upper = opt.map(|s| s.to_uppercase()); // => Some("HELLO")let opt = Some("42");
let parsed = opt.and_then(|s| s.parse::<i32>().ok()); // => Some(42)let name: Option<&str> = Some("Alice");
let result: Result<&str, &str> = name.ok_or("name required");
// ok_or_else: lazy error (only created on None)
let result = name.ok_or_else(|| format!("missing at line {}", n));Use ok_or for cheap static errors. Use ok_or_else when the error involves allocation.
let timeout = config.get("timeout")
.and_then(|v| v.parse::<u64>().ok())
.unwrap_or(30); // default 30let even = Some(4).filter(|&x| x % 2 == 0); // => Some(4)
let odd = Some(3).filter(|&x| x % 2 == 0); // => NoneConverting Error Types
The ? operator auto-converts errors via the From trait. Use map_err for manual conversion.
fn load_port() -> Result<u16, String> {
let text = std::fs::read_to_string("port.txt")
.map_err(|e| format!("read failed: {e}"))?;
let port = text.trim().parse::<u16>()
.map_err(|e| format!("bad port: {e}"))?;
Ok(port)
}// If AppError implements From<io::Error>,
// then ? converts automatically:
fn load() -> Result<String, AppError> {
let s = std::fs::read_to_string("file.txt")?; // io::Error -> AppError
Ok(s)
}use std::error::Error;
fn run() -> Result<(), Box<dyn Error>> {
let text = std::fs::read_to_string("data.txt")?; // io::Error
let num: i32 = text.trim().parse()?; // ParseIntError
println!("{num}");
Ok(())
}Box<dyn Error> is great for applications. For libraries, use concrete error types so callers can match.
Custom Error Types
Define an error enum with From impls so ? auto-converts. Use thiserror to reduce boilerplate.
use std::fmt;
#[derive(Debug)]
enum AppError {
Io(std::io::Error),
Parse(std::num::ParseIntError),
Config(String),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::Io(e) => write!(f, "IO: {e}"),
AppError::Parse(e) => write!(f, "parse: {e}"),
AppError::Config(msg) => write!(f, "config: {msg}"),
}
}
}
impl From<std::io::Error> for AppError {
fn from(e: std::io::Error) -> Self { AppError::Io(e) }
}use thiserror::Error;
#[derive(Debug, Error)]
enum AppError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("parse error: {0}")]
Parse(#[from] std::num::ParseIntError),
#[error("config error: {0}")]
Config(String),
}thiserror generates Display and From impls from attributes. Use for library error types.
anyhow for Applications
The anyhow crate provides ergonomic error handling for applications (not libraries).
use anyhow::{Context, Result};
fn load_config() -> Result<Config> {
let text = std::fs::read_to_string("config.toml")
.context("failed to read config file")?;
let config: Config = toml::from_str(&text)
.context("failed to parse config")?;
Ok(config)
}use anyhow::{bail, ensure, Result};
fn validate(port: u16) -> Result<()> {
ensure!(port > 0, "port must be positive");
if port > 65535 {
bail!("port {port} out of range");
}
Ok(())
}bail! returns Err immediately. ensure! is like assert but returns Err instead of panicking.
panic! vs Result
panic! is for bugs and impossible states. Result is for expected failures the caller can handle.
// Impossible state: logic error, not user input
fn index_twice(slice: &[i32], i: usize, j: usize) -> (i32, i32) {
assert!(i != j, "indices must differ");
(slice[i], slice[j])
}
// Violated contract in unsafe code
// Array out of bounds (bug, not user error)// User input might be invalid
fn parse_age(input: &str) -> Result<u32, String> {
input.parse::<u32>()
.map_err(|_| format!("invalid age: {input}"))
}
// File might not exist
fn read_config() -> Result<String, std::io::Error> {
std::fs::read_to_string("config.toml")
}// panic! => bug in your code (should never happen)
// Result => expected failure (bad input, missing file, network error)
// Option => value might be absent (not an error, just missing)Collecting Results
collect() can turn an iterator of Results into a Result of a collection. It short-circuits on the first Err.
let strings = vec!["1", "2", "three", "4"];
let numbers: Result<Vec<i32>, _> = strings
.iter()
.map(|s| s.parse::<i32>())
.collect();
// => Err(ParseIntError) -- stops at "three"let numbers: Vec<i32> = strings
.iter()
.filter_map(|s| s.parse::<i32>().ok())
.collect();
// => [1, 2, 4] -- "three" silently skippedlet (oks, errs): (Vec<_>, Vec<_>) = strings
.iter()
.map(|s| s.parse::<i32>())
.partition(Result::is_ok);
let numbers: Vec<i32> = oks.into_iter().map(Result::unwrap).collect();
let errors: Vec<_> = errs.into_iter().map(Result::unwrap_err).collect();Can you write this from memory?
Write a function signature for parse_number that takes a &str and returns Result<i32, String>.