Rust Error Handling
Overview
Rust adopts a unique approach to error handling, using the type system to handle errors rather than exception mechanisms. This chapter will learn about error handling patterns in Rust, including the Result and Option types, as well as error propagation and custom error types.
🎯 Rust Error Handling Philosophy
Error Classification
rust
// Rust categorizes errors into two types:
// 1. Recoverable Errors - use Result<T, E>
// 2. Unrecoverable Errors - use panic!
fn error_classification() {
// Recoverable error example
let result = std::fs::read_to_string("maybe_exists.txt");
match result {
Ok(content) => println!("File content: {}", content),
Err(error) => println!("Failed to read file: {}", error),
}
// Unrecoverable error example
let numbers = vec![1, 2, 3];
// let index = numbers[10]; // This will panic!
// Manually trigger panic
if numbers.is_empty() {
panic!("Vector cannot be empty!");
}
}📦 Option Type
Option Basics
rust
fn option_basics() {
// Option represents a value that may or may not exist
let some_number = Some(5);
let some_string = Some("string");
let absent_number: Option<i32> = None;
println!("Has value: {:?}", some_number);
println!("No value: {:?}", absent_number);
// Use match to handle Option
let x = Some(5);
match x {
Some(value) => println!("Value is: {}", value),
None => println!("No value"),
}
// Use if let to simplify
if let Some(value) = some_number {
println!("Found value: {}", value);
}
}Common Option Methods
rust
fn option_methods() {
let x = Some(2);
let y: Option<i32> = None;
// is_some() and is_none()
println!("x has value? {}", x.is_some());
println!("y has no value? {}", y.is_none());
// unwrap() - dangerous method, may panic
// let value = y.unwrap(); // This will panic!
// unwrap_or() - provide default value
let default_value = y.unwrap_or(0);
println!("Default value: {}", default_value);
// unwrap_or_else() - use closure to compute default value
let computed_default = y.unwrap_or_else(|| {
println!("Computing default value");
42
});
println!("Computed default: {}", computed_default);
// expect() - unwrap with custom error message
let value = x.expect("x should have a value");
println!("Expected value: {}", value);
// map() - transform value inside Option
let doubled = x.map(|v| v * 2);
println!("Doubled: {:?}", doubled);
// and_then() - chain operations
let result = x.and_then(|v| {
if v > 0 {
Some(v * 10)
} else {
None
}
});
println!("Chained result: {:?}", result);
// filter() - conditional filtering
let filtered = x.filter(|&v| v > 1);
println!("Filtered result: {:?}", filtered);
}Real-World Option Applications
rust
fn find_user_by_id(id: u32) -> Option<User> {
let users = vec![
User { id: 1, name: "Alice".to_string() },
User { id: 2, name: "Bob".to_string() },
User { id: 3, name: "Charlie".to_string() },
];
users.into_iter().find(|user| user.id == id)
}
#[derive(Debug)]
struct User {
id: u32,
name: String,
}
fn option_real_world() {
// Find user
match find_user_by_id(2) {
Some(user) => println!("Found user: {:?}", user),
None => println!("User does not exist"),
}
// Parse string
let number_str = "42";
let parsed = number_str.parse::<i32>().ok(); // Result to Option
println!("Parse result: {:?}", parsed);
// Array access
let numbers = vec![1, 2, 3, 4, 5];
let first = numbers.get(0);
let out_of_bounds = numbers.get(10);
println!("First element: {:?}", first);
println!("Out of bounds: {:?}", out_of_bounds);
}⚠️ Result Type
Result Basics
rust
// Result<T, E> represents an operation that may succeed or fail
fn divide(dividend: f64, divisor: f64) -> Result<f64, String> {
if divisor == 0.0 {
Err("Cannot divide by zero".to_string())
} else {
Ok(dividend / divisor)
}
}
fn result_basics() {
let success_result = divide(10.0, 2.0);
let error_result = divide(10.0, 0.0);
// Use match to handle Result
match success_result {
Ok(value) => println!("Result: {}", value),
Err(error) => println!("Error: {}", error),
}
match error_result {
Ok(value) => println!("Result: {}", value),
Err(error) => println!("Error: {}", error),
}
// Use if let to simplify
if let Ok(value) = divide(20.0, 4.0) {
println!("Division result: {}", value);
}
if let Err(error) = divide(10.0, 0.0) {
println!("Division error: {}", error);
}
}Common Result Methods
rust
fn result_methods() {
let success: Result<i32, &str> = Ok(42);
let failure: Result<i32, &str> = Err("Something went wrong");
// is_ok() and is_err()
println!("Success? {}", success.is_ok());
println!("Failure? {}", failure.is_err());
// unwrap() and expect()
let value = success.unwrap();
println!("Unwrapped value: {}", value);
// let error_value = failure.unwrap(); // This will panic!
let expected_value = success.expect("Should succeed");
println!("Expected value: {}", expected_value);
// unwrap_or() and unwrap_or_else()
let default_value = failure.unwrap_or(0);
println!("Default value: {}", default_value);
let computed_value = failure.unwrap_or_else(|error| {
println!("Handling error: {}", error);
-1
});
println!("Computed value: {}", computed_value);
// map() and map_err()
let doubled = success.map(|v| v * 2);
println!("Doubled result: {:?}", doubled);
let mapped_error = failure.map_err(|e| format!("Error: {}", e));
println!("Mapped error: {:?}", mapped_error);
// and_then() - chain operations
let chained = success.and_then(|v| {
if v > 0 {
Ok(v.to_string())
} else {
Err("Value must be greater than 0")
}
});
println!("Chained result: {:?}", chained);
}🔄 Error Propagation
The ? Operator
rust
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let mut f = File::open("username.txt")?; // ? operator
let mut s = String::new();
f.read_to_string(&mut s)?; // ? operator
Ok(s)
}
// Equivalent match version
fn read_username_from_file_verbose() -> Result<String, io::Error> {
let f = File::open("username.txt");
let mut f = match f {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut s = String::new();
match f.read_to_string(&mut s) {
Ok(_) => Ok(s),
Err(e) => Err(e),
}
}
// More concise version
fn read_username_from_file_short() -> Result<String, io::Error> {
let mut s = String::new();
File::open("username.txt")?.read_to_string(&mut s)?;
Ok(s)
}
// Shortest version
fn read_username_from_file_shortest() -> Result<String, io::Error> {
std::fs::read_to_string("username.txt")
}Error Propagation Chaining
rust
fn error_propagation_chain() -> Result<i32, Box<dyn std::error::Error>> {
let content = std::fs::read_to_string("numbers.txt")?;
let number: i32 = content.trim().parse()?;
let result = divide_by_two(number)?;
Ok(result)
}
fn divide_by_two(n: i32) -> Result<i32, String> {
if n % 2 != 0 {
Err("Number must be even".to_string())
} else {
Ok(n / 2)
}
}
fn demonstration_error_propagation() {
match error_propagation_chain() {
Ok(result) => println!("Final result: {}", result),
Err(error) => println!("Error: {}", error),
}
}🛠️ Custom Error Types
Simple Custom Error
rust
#[derive(Debug)]
enum CalculatorError {
DivisionByZero,
InvalidInput,
Overflow,
}
impl std::fmt::Display for CalculatorError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
CalculatorError::DivisionByZero => write!(f, "Cannot divide by zero"),
CalculatorError::InvalidInput => write!(f, "Invalid input"),
CalculatorError::Overflow => write!(f, "Numeric overflow"),
}
}
}
impl std::error::Error for CalculatorError {}
fn safe_divide(dividend: i32, divisor: i32) -> Result<i32, CalculatorError> {
if divisor == 0 {
return Err(CalculatorError::DivisionByZero);
}
match dividend.checked_div(divisor) {
Some(result) => Ok(result),
None => Err(CalculatorError::Overflow),
}
}
fn custom_error_example() {
let operations = vec![
(10, 2),
(10, 0),
(i32::MIN, -1),
];
for (a, b) in operations {
match safe_divide(a, b) {
Ok(result) => println!("{} / {} = {}", a, b, result),
Err(error) => println!("{} / {} failed: {}", a, b, error),
}
}
}Complex Error Types
rust
use std::fmt;
#[derive(Debug)]
struct ParseError {
input: String,
kind: ParseErrorKind,
}
#[derive(Debug)]
enum ParseErrorKind {
EmptyInput,
InvalidCharacter(char),
NumberTooLarge,
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Error parsing '{}': ", self.input)?;
match &self.kind {
ParseErrorKind::EmptyInput => write!(f, "Input is empty"),
ParseErrorKind::InvalidCharacter(c) => write!(f, "Invalid character '{}'", c),
ParseErrorKind::NumberTooLarge => write!(f, "Number is too large"),
}
}
}
impl std::error::Error for ParseError {}
fn parse_positive_number(input: &str) -> Result<u32, ParseError> {
if input.is_empty() {
return Err(ParseError {
input: input.to_string(),
kind: ParseErrorKind::EmptyInput,
});
}
for ch in input.chars() {
if !ch.is_ascii_digit() {
return Err(ParseError {
input: input.to_string(),
kind: ParseErrorKind::InvalidCharacter(ch),
});
}
}
match input.parse::<u32>() {
Ok(number) => Ok(number),
Err(_) => Err(ParseError {
input: input.to_string(),
kind: ParseErrorKind::NumberTooLarge,
}),
}
}
fn complex_error_example() {
let test_inputs = vec!["123", "", "12a", "999999999999999999999"];
for input in test_inputs {
match parse_positive_number(input) {
Ok(number) => println!("Parse successful: {} -> {}", input, number),
Err(error) => println!("Parse failed: {}", error),
}
}
}Error Conversion and Compatibility
rust
use std::num::ParseIntError;
#[derive(Debug)]
enum AppError {
Io(std::io::Error),
Parse(ParseIntError),
Custom(String),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
AppError::Io(err) => write!(f, "IO error: {}", err),
AppError::Parse(err) => write!(f, "Parse error: {}", err),
AppError::Custom(msg) => write!(f, "Custom error: {}", msg),
}
}
}
impl std::error::Error for AppError {}
// Automatic conversion
impl From<std::io::Error> for AppError {
fn from(error: std::io::Error) -> Self {
AppError::Io(error)
}
}
impl From<ParseIntError> for AppError {
fn from(error: ParseIntError) -> Self {
AppError::Parse(error)
}
}
fn process_file(filename: &str) -> Result<i32, AppError> {
let content = std::fs::read_to_string(filename)?; // Auto-convert io::Error
let number: i32 = content.trim().parse()?; // Auto-convert ParseIntError
if number < 0 {
return Err(AppError::Custom("Number cannot be negative".to_string()));
}
Ok(number * 2)
}
fn error_conversion_example() {
match process_file("number.txt") {
Ok(result) => println!("Processing result: {}", result),
Err(error) => println!("Processing failed: {}", error),
}
}🎭 Advanced Error Handling Patterns
Handling Multiple Error Types
rust
use std::collections::HashMap;
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
struct UserManager {
users: HashMap<u32, User>,
}
impl UserManager {
fn new() -> Self {
Self {
users: HashMap::new(),
}
}
fn add_user(&mut self, id: u32, name: String) -> Result<()> {
if self.users.contains_key(&id) {
return Err("User ID already exists".into());
}
if name.is_empty() {
return Err("User name cannot be empty".into());
}
self.users.insert(id, User { id, name });
Ok(())
}
fn get_user(&self, id: u32) -> Result<&User> {
self.users.get(&id).ok_or_else(|| "User does not exist".into())
}
fn save_to_file(&self, filename: &str) -> Result<()> {
let json = serde_json::to_string(&self.users)?;
std::fs::write(filename, json)?;
Ok(())
}
fn load_from_file(&mut self, filename: &str) -> Result<()> {
let content = std::fs::read_to_string(filename)?;
self.users = serde_json::from_str(&content)?;
Ok(())
}
}
fn advanced_error_handling() {
let mut manager = UserManager::new();
// Chained error handling
let result = manager
.add_user(1, "Alice".to_string())
.and_then(|_| manager.add_user(2, "Bob".to_string()))
.and_then(|_| manager.save_to_file("users.json"));
match result {
Ok(_) => println!("Operation successful"),
Err(error) => println!("Operation failed: {}", error),
}
}Recovery Strategies
rust
fn retry_with_backoff<F, T, E>(mut operation: F, max_retries: u32) -> Result<T, E>
where
F: FnMut() -> Result<T, E>,
E: std::fmt::Debug,
{
let mut delay = std::time::Duration::from_millis(100);
for attempt in 0..=max_retries {
match operation() {
Ok(result) => return Ok(result),
Err(error) => {
if attempt == max_retries {
return Err(error);
}
println!("Attempt {} failed: {:?}, retrying in {}ms", attempt + 1, error, delay.as_millis());
std::thread::sleep(delay);
delay *= 2; // Exponential backoff
}
}
}
unreachable!()
}
fn unreliable_operation() -> Result<String, String> {
use rand::Rng;
let mut rng = rand::thread_rng();
if rng.gen_bool(0.7) { // 70% failure rate
Err("Network connection failed".to_string())
} else {
Ok("Operation successful".to_string())
}
}
fn recovery_strategy_example() {
match retry_with_backoff(unreliable_operation, 3) {
Ok(result) => println!("Finally succeeded: {}", result),
Err(error) => println!("Finally failed: {}", error),
}
}📝 Chapter Summary
Through this chapter, you should have mastered:
Basic Error Handling
- ✅ Using Option and Result types
- ✅ Various error handling methods
- ✅ Error propagation with ? operator
- ✅ Choosing between match and if let
Advanced Error Handling
- ✅ Designing custom error types
- ✅ Error conversion and compatibility
- ✅ Unified handling of multiple error types
- ✅ Error recovery strategies
Best Practices
- Prefer Result over panic
- Design clear error type hierarchies
- Provide meaningful error messages
- Use ? operator appropriately
- Consider error recoverability
Continue Learning: Next Chapter - Rust Generics and Traits