Error Handling#
- Source:
Rust uses Result<T, E> and Option<T> for error handling instead of exceptions.
This approach makes error handling explicit - you can see from a function’s signature
whether it can fail and what errors it might return. Unlike exceptions which can be
thrown from anywhere and caught anywhere (or not caught at all), Rust errors are values
that must be explicitly handled at each call site. This eliminates the “invisible control
flow” problem of exceptions and makes code easier to reason about.
Error Handling Comparison#
The following table maps C++ error handling mechanisms to their Rust equivalents. Note
that Rust’s Result is similar to C++23’s std::expected, but Result has been
in Rust since 1.0 and is deeply integrated into the language with the ? operator:
C++ |
Rust |
|---|---|
|
|
|
|
|
|
|
|
|
|
Option Type#
Option<T> represents a value that may or may not exist - it’s either Some(value)
or None. This replaces null pointers and eliminates null pointer dereference bugs
at compile time. Unlike C++ where you might forget to check if an optional has a
value before accessing it, Rust forces you to handle both cases through pattern matching
or methods like unwrap_or. The compiler won’t let you access the inner value without
acknowledging that it might not exist:
C++:
#include <optional>
std::optional<int> find_index(const std::vector<int>& v, int target) {
for (size_t i = 0; i < v.size(); i++) {
if (v[i] == target) return i;
}
return std::nullopt;
}
int main() {
std::vector<int> v = {1, 2, 3};
if (auto idx = find_index(v, 2)) {
std::cout << "Found at " << *idx;
}
}
Rust:
fn find_index(v: &[i32], target: i32) -> Option<usize> {
for (i, &x) in v.iter().enumerate() {
if x == target {
return Some(i);
}
}
None
}
fn main() {
let v = vec![1, 2, 3];
if let Some(idx) = find_index(&v, 2) {
println!("Found at {}", idx);
}
}
Option Methods#
Option provides many methods for working with optional values without verbose
pattern matching. These methods form a fluent API for transforming, combining, and
extracting optional values. The unwrap family of methods extracts the value but
panics if it’s None - use these only when you’re certain the value exists or in
prototyping code:
let x: Option<i32> = Some(42);
let y: Option<i32> = None;
// Unwrapping
let val = x.unwrap(); // panics if None
let val = x.expect("no value"); // panics with message
let val = x.unwrap_or(0); // default if None
let val = x.unwrap_or_default(); // Default::default() if None
let val = x.unwrap_or_else(|| compute_default());
// Checking
let is_some = x.is_some();
let is_none = y.is_none();
// Transforming
let doubled = x.map(|v| v * 2); // Some(84)
let filtered = x.filter(|&v| v > 50); // None
let chained = x.and_then(|v| Some(v + 1)); // Some(43)
// Converting to Result
let result: Result<i32, &str> = x.ok_or("no value");
Result Type#
Result<T, E> represents an operation that can succeed with a value of type T
or fail with an error of type E. Unlike exceptions which separate the error path
from the normal code path, Result keeps them together - you handle the error right
where it occurs. This makes error handling more predictable and prevents errors from
silently propagating up the call stack. The type system ensures you can’t accidentally
ignore an error:
C++:
#include <expected> // C++23
#include <string>
std::expected<int, std::string> parse_int(const std::string& s) {
try {
return std::stoi(s);
} catch (...) {
return std::unexpected("parse error");
}
}
Rust:
fn parse_int(s: &str) -> Result<i32, std::num::ParseIntError> {
s.parse()
}
fn main() {
match parse_int("42") {
Ok(n) => println!("Parsed: {}", n),
Err(e) => println!("Error: {}", e),
}
}
Result Methods#
Result provides methods similar to Option for working with fallible operations.
The map and map_err methods let you transform the success or error value
independently. The and_then method (also called flatmap in other languages)
chains operations that might fail:
let x: Result<i32, &str> = Ok(42);
let y: Result<i32, &str> = Err("error");
// Unwrapping
let val = x.unwrap(); // panics if Err
let val = x.expect("failed"); // panics with message
let val = x.unwrap_or(0); // default if Err
let val = x.unwrap_or_else(|e| handle_error(e));
// Checking
let is_ok = x.is_ok();
let is_err = y.is_err();
// Transforming
let doubled = x.map(|v| v * 2); // Ok(84)
let mapped_err = y.map_err(|e| format!("Error: {}", e));
// Converting to Option
let opt: Option<i32> = x.ok(); // Some(42)
let opt: Option<&str> = y.err(); // Some("error")
The ? Operator#
The ? operator is Rust’s way of propagating errors concisely. When applied to a
Result, it either unwraps the Ok value and continues, or returns early with
the Err value. This eliminates the boilerplate of matching on every Result
and manually returning errors. The ? operator also performs automatic error type
conversion using the From trait, making it easy to combine different error types:
C++ (manual propagation):
std::expected<int, Error> read_config() {
auto file = open_file("config.txt");
if (!file) return std::unexpected(file.error());
auto content = read_content(*file);
if (!content) return std::unexpected(content.error());
auto value = parse_int(*content);
if (!value) return std::unexpected(value.error());
return *value;
}
Rust (with ?):
fn read_config() -> Result<i32, Error> {
let file = open_file("config.txt")?;
let content = read_content(&file)?;
let value = parse_int(&content)?;
Ok(value)
}
// ? works with Option too
fn first_char(s: &str) -> Option<char> {
let c = s.chars().next()?;
Some(c)
}
Custom Error Types#
For libraries and larger applications, you’ll want to define custom error types that
can represent all the ways your code can fail. A common pattern is an enum with variants
for each error kind. Implementing From for other error types enables automatic
conversion with the ? operator, so you can use ? on standard library errors
and have them automatically converted to your custom type:
use std::fmt;
#[derive(Debug)]
enum AppError {
IoError(std::io::Error),
ParseError(std::num::ParseIntError),
Custom(String),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
AppError::IoError(e) => write!(f, "IO error: {}", e),
AppError::ParseError(e) => write!(f, "Parse error: {}", e),
AppError::Custom(s) => write!(f, "{}", s),
}
}
}
impl std::error::Error for AppError {}
// Implement From for automatic conversion with ?
impl From<std::io::Error> for AppError {
fn from(e: std::io::Error) -> Self {
AppError::IoError(e)
}
}
Pattern Matching Errors#
Pattern matching gives you fine-grained control over error handling. You can match
on specific error variants, use guards to add conditions, and handle multiple cases
differently. The if let syntax is convenient when you only care about one case,
and let else (Rust 1.65+) provides early return for the error case:
fn process(input: &str) -> Result<i32, String> {
match input.parse::<i32>() {
Ok(n) if n > 0 => Ok(n * 2),
Ok(_) => Err("must be positive".into()),
Err(e) => Err(format!("parse error: {}", e)),
}
}
// if let for single case
if let Ok(n) = "42".parse::<i32>() {
println!("{}", n);
}
// let else for early return
fn process2(input: &str) -> i32 {
let Ok(n) = input.parse::<i32>() else {
return 0;
};
n * 2
}
Panic vs Result#
Rust distinguishes between recoverable errors (Result) and unrecoverable errors
(panic!). Use panic! for bugs and invariant violations - situations that
indicate a programming error rather than an expected failure. Use Result for
operations that can legitimately fail, like file I/O or parsing user input. The
assert! macro panics in debug builds but can be compiled out in release builds
for performance-critical code:
// panic! - unrecoverable errors (bugs, invariant violations)
fn get_index(v: &[i32], i: usize) -> i32 {
if i >= v.len() {
panic!("index out of bounds");
}
v[i]
}
// Result - recoverable errors (expected failures)
fn read_file(path: &str) -> Result<String, std::io::Error> {
std::fs::read_to_string(path)
}
// assert! - debug checks (removed in release)
fn divide(a: i32, b: i32) -> i32 {
debug_assert!(b != 0, "division by zero");
a / b
}
See Also#
Traits and Generics - Error trait
Iterators - Iterator methods returning Option/Result