Rust Error Handling Best Practices¶
Objective: Master senior-level Rust error handling patterns for production systems. When you need to build robust error handling, when you want to create maintainable error propagation, when you need enterprise-grade error managementโthese best practices become your weapon of choice.
Core Principles¶
- Explicit Error Handling: Use Result and Option types
- Error Propagation: Chain errors with ? operator
- Custom Error Types: Create domain-specific error types
- Error Context: Provide meaningful error information
- Recovery Strategies: Implement error recovery patterns
Result and Option Patterns¶
Basic Error Handling¶
// rust/01-basic-error-handling.rs
/*
Basic error handling patterns with Result and Option
*/
use std::fs::File;
use std::io::{self, Read};
use std::num::ParseIntError;
use std::str::FromStr;
/// Demonstrates basic Result patterns.
pub struct DataProcessor {
data: Vec<String>,
}
impl DataProcessor {
pub fn new() -> Self {
Self { data: Vec::new() }
}
/// Processes data with error handling.
pub fn process_data(&mut self, input: &str) -> Result<usize, String> {
if input.is_empty() {
return Err("Input cannot be empty".to_string());
}
let processed = input.to_uppercase();
self.data.push(processed);
Ok(self.data.len())
}
/// Processes data with Option return.
pub fn get_data(&self, index: usize) -> Option<&String> {
self.data.get(index)
}
/// Processes data with Result and error propagation.
pub fn process_file(&self, filename: &str) -> Result<String, io::Error> {
let mut file = File::open(filename)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
/// Processes data with custom error handling.
pub fn process_number(&self, input: &str) -> Result<i32, ParseIntError> {
input.parse::<i32>()
}
/// Processes data with error mapping.
pub fn process_number_with_context(&self, input: &str) -> Result<i32, String> {
input.parse::<i32>()
.map_err(|e| format!("Failed to parse '{}' as integer: {}", input, e))
}
}
/// Demonstrates Option patterns.
pub struct OptionDemo {
data: Vec<Option<String>>,
}
impl OptionDemo {
pub fn new() -> Self {
Self { data: Vec::new() }
}
/// Adds data with Option handling.
pub fn add_data(&mut self, data: Option<String>) {
self.data.push(data);
}
/// Gets data with Option handling.
pub fn get_data(&self, index: usize) -> Option<&String> {
self.data.get(index)?.as_ref()
}
/// Processes data with Option chaining.
pub fn process_data(&self, index: usize) -> Option<String> {
self.data.get(index)?
.as_ref()
.map(|s| s.to_uppercase())
}
/// Processes data with Option and default values.
pub fn get_data_or_default(&self, index: usize) -> String {
self.data.get(index)
.and_then(|opt| opt.as_ref())
.map(|s| s.clone())
.unwrap_or_else(|| "default".to_string())
}
}
/// Demonstrates Result patterns with error chaining.
pub struct ResultDemo {
data: Vec<Result<String, String>>,
}
impl ResultDemo {
pub fn new() -> Self {
Self { data: Vec::new() }
}
/// Adds data with Result handling.
pub fn add_data(&mut self, data: Result<String, String>) {
self.data.push(data);
}
/// Gets data with Result handling.
pub fn get_data(&self, index: usize) -> Result<&String, &String> {
match self.data.get(index) {
Some(Ok(data)) => Ok(data),
Some(Err(error)) => Err(error),
None => Err(&"Index out of bounds".to_string()),
}
}
/// Processes data with Result chaining.
pub fn process_data(&self, index: usize) -> Result<String, String> {
let data = self.data.get(index)
.ok_or("Index out of bounds")?;
let value = data.as_ref()
.map_err(|e| format!("Error in data: {}", e))?;
Ok(value.to_uppercase())
}
/// Processes data with Result and error mapping.
pub fn process_data_with_context(&self, index: usize) -> Result<String, String> {
let data = self.data.get(index)
.ok_or_else(|| format!("Index {} out of bounds", index))?;
let value = data.as_ref()
.map_err(|e| format!("Error in data at index {}: {}", index, e))?;
Ok(value.to_uppercase())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_data_processor() {
let mut processor = DataProcessor::new();
let result = processor.process_data("hello");
assert!(result.is_ok());
assert_eq!(result.unwrap(), 1);
let result = processor.process_data("");
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "Input cannot be empty");
}
#[test]
fn test_option_demo() {
let mut demo = OptionDemo::new();
demo.add_data(Some("hello".to_string()));
demo.add_data(None);
assert_eq!(demo.get_data(0), Some(&"hello".to_string()));
assert_eq!(demo.get_data(1), None);
assert_eq!(demo.process_data(0), Some("HELLO".to_string()));
assert_eq!(demo.process_data(1), None);
assert_eq!(demo.get_data_or_default(0), "hello");
assert_eq!(demo.get_data_or_default(1), "default");
}
#[test]
fn test_result_demo() {
let mut demo = ResultDemo::new();
demo.add_data(Ok("hello".to_string()));
demo.add_data(Err("error".to_string()));
assert_eq!(demo.get_data(0), Ok(&"hello".to_string()));
assert_eq!(demo.get_data(1), Err(&"error".to_string()));
assert_eq!(demo.process_data(0), Ok("HELLO".to_string()));
assert_eq!(demo.process_data(1), Err("Error in data: error".to_string()));
}
}
Custom Error Types¶
// rust/02-custom-error-types.rs
/*
Custom error types and error handling patterns
*/
use std::fmt;
use std::io;
use std::num::ParseIntError;
use thiserror::Error;
/// Custom error type for data processing.
#[derive(Error, Debug)]
pub enum DataProcessingError {
#[error("Input validation failed: {0}")]
Validation(String),
#[error("Processing failed: {0}")]
Processing(String),
#[error("IO error: {0}")]
Io(#[from] io::Error),
#[error("Parse error: {0}")]
Parse(#[from] ParseIntError),
#[error("Unknown error: {0}")]
Unknown(String),
}
/// Custom error type for business logic.
#[derive(Error, Debug)]
pub enum BusinessError {
#[error("User not found: {user_id}")]
UserNotFound { user_id: u64 },
#[error("Insufficient permissions for user: {user_id}")]
InsufficientPermissions { user_id: u64 },
#[error("Resource already exists: {resource_name}")]
ResourceExists { resource_name: String },
#[error("Business rule violation: {rule}")]
BusinessRuleViolation { rule: String },
#[error("External service error: {service} - {message}")]
ExternalService { service: String, message: String },
}
/// Custom error type for API operations.
#[derive(Error, Debug)]
pub enum ApiError {
#[error("Bad request: {0}")]
BadRequest(String),
#[error("Unauthorized: {0}")]
Unauthorized(String),
#[error("Forbidden: {0}")]
Forbidden(String),
#[error("Not found: {0}")]
NotFound(String),
#[error("Internal server error: {0}")]
InternalServerError(String),
#[error("Service unavailable: {0}")]
ServiceUnavailable(String),
}
impl ApiError {
/// Get HTTP status code for the error.
pub fn status_code(&self) -> u16 {
match self {
ApiError::BadRequest(_) => 400,
ApiError::Unauthorized(_) => 401,
ApiError::Forbidden(_) => 403,
ApiError::NotFound(_) => 404,
ApiError::InternalServerError(_) => 500,
ApiError::ServiceUnavailable(_) => 503,
}
}
}
/// Demonstrates custom error handling.
pub struct CustomErrorDemo {
data: Vec<String>,
}
impl CustomErrorDemo {
pub fn new() -> Self {
Self { data: Vec::new() }
}
/// Processes data with custom error handling.
pub fn process_data(&mut self, input: &str) -> Result<usize, DataProcessingError> {
if input.is_empty() {
return Err(DataProcessingError::Validation("Input cannot be empty".to_string()));
}
if input.len() > 100 {
return Err(DataProcessingError::Validation("Input too long".to_string()));
}
let processed = input.to_uppercase();
self.data.push(processed);
Ok(self.data.len())
}
/// Processes data with business error handling.
pub fn process_business_data(&self, user_id: u64, data: &str) -> Result<String, BusinessError> {
if user_id == 0 {
return Err(BusinessError::UserNotFound { user_id });
}
if user_id < 1000 {
return Err(BusinessError::InsufficientPermissions { user_id });
}
if data.is_empty() {
return Err(BusinessError::BusinessRuleViolation {
rule: "Data cannot be empty".to_string(),
});
}
Ok(data.to_uppercase())
}
/// Processes data with API error handling.
pub fn process_api_data(&self, data: &str) -> Result<String, ApiError> {
if data.is_empty() {
return Err(ApiError::BadRequest("Data cannot be empty".to_string()));
}
if data.len() > 1000 {
return Err(ApiError::BadRequest("Data too long".to_string()));
}
if data.contains("error") {
return Err(ApiError::InternalServerError("Data contains error".to_string()));
}
Ok(data.to_uppercase())
}
/// Processes data with error chaining.
pub fn process_data_with_chain(&mut self, input: &str) -> Result<usize, DataProcessingError> {
let processed = self.process_data(input)?;
Ok(processed)
}
/// Processes data with error mapping.
pub fn process_data_with_mapping(&mut self, input: &str) -> Result<usize, String> {
self.process_data(input)
.map_err(|e| format!("Failed to process data: {}", e))
}
}
/// Demonstrates error recovery patterns.
pub struct ErrorRecoveryDemo {
data: Vec<String>,
retry_count: u32,
max_retries: u32,
}
impl ErrorRecoveryDemo {
pub fn new(max_retries: u32) -> Self {
Self {
data: Vec::new(),
retry_count: 0,
max_retries,
}
}
/// Processes data with retry logic.
pub fn process_data_with_retry(&mut self, input: &str) -> Result<usize, String> {
self.retry_count = 0;
loop {
match self.process_data_internal(input) {
Ok(result) => return Ok(result),
Err(error) => {
self.retry_count += 1;
if self.retry_count >= self.max_retries {
return Err(format!("Max retries exceeded: {}", error));
}
}
}
}
}
/// Internal processing method.
fn process_data_internal(&mut self, input: &str) -> Result<usize, String> {
if input.is_empty() {
return Err("Input cannot be empty".to_string());
}
if input.len() > 100 {
return Err("Input too long".to_string());
}
let processed = input.to_uppercase();
self.data.push(processed);
Ok(self.data.len())
}
/// Processes data with fallback.
pub fn process_data_with_fallback(&mut self, input: &str) -> Result<usize, String> {
match self.process_data_internal(input) {
Ok(result) => Ok(result),
Err(_) => {
// Fallback to default processing
self.data.push("default".to_string());
Ok(self.data.len())
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_custom_error_demo() {
let mut demo = CustomErrorDemo::new();
let result = demo.process_data("hello");
assert!(result.is_ok());
assert_eq!(result.unwrap(), 1);
let result = demo.process_data("");
assert!(result.is_err());
match result.unwrap_err() {
DataProcessingError::Validation(msg) => assert_eq!(msg, "Input cannot be empty"),
_ => panic!("Expected Validation error"),
}
}
#[test]
fn test_business_error_handling() {
let demo = CustomErrorDemo::new();
let result = demo.process_business_data(1001, "hello");
assert!(result.is_ok());
assert_eq!(result.unwrap(), "HELLO");
let result = demo.process_business_data(0, "hello");
assert!(result.is_err());
match result.unwrap_err() {
BusinessError::UserNotFound { user_id } => assert_eq!(user_id, 0),
_ => panic!("Expected UserNotFound error"),
}
}
#[test]
fn test_api_error_handling() {
let demo = CustomErrorDemo::new();
let result = demo.process_api_data("hello");
assert!(result.is_ok());
assert_eq!(result.unwrap(), "HELLO");
let result = demo.process_api_data("");
assert!(result.is_err());
match result.unwrap_err() {
ApiError::BadRequest(msg) => assert_eq!(msg, "Data cannot be empty"),
_ => panic!("Expected BadRequest error"),
}
}
#[test]
fn test_error_recovery() {
let mut demo = ErrorRecoveryDemo::new(3);
let result = demo.process_data_with_retry("hello");
assert!(result.is_ok());
assert_eq!(result.unwrap(), 1);
let result = demo.process_data_with_fallback("");
assert!(result.is_ok());
assert_eq!(result.unwrap(), 2);
}
}
Error Propagation Patterns¶
// rust/03-error-propagation.rs
/*
Error propagation patterns and best practices
*/
use std::fs::File;
use std::io::{self, Read, Write};
use std::path::Path;
use anyhow::{Context, Result as AnyhowResult};
use thiserror::Error;
/// Custom error type for file operations.
#[derive(Error, Debug)]
pub enum FileError {
#[error("File not found: {path}")]
NotFound { path: String },
#[error("Permission denied: {path}")]
PermissionDenied { path: String },
#[error("IO error: {0}")]
Io(#[from] io::Error),
#[error("Invalid file format: {0}")]
InvalidFormat(String),
}
/// Demonstrates error propagation with ? operator.
pub struct ErrorPropagationDemo {
data: Vec<String>,
}
impl ErrorPropagationDemo {
pub fn new() -> Self {
Self { data: Vec::new() }
}
/// Processes file with error propagation.
pub fn process_file(&mut self, filename: &str) -> Result<usize, FileError> {
let content = self.read_file(filename)?;
let processed = self.process_content(&content)?;
self.data.push(processed);
Ok(self.data.len())
}
/// Reads file with error propagation.
fn read_file(&self, filename: &str) -> Result<String, FileError> {
let path = Path::new(filename);
if !path.exists() {
return Err(FileError::NotFound {
path: filename.to_string(),
});
}
let mut file = File::open(filename)?;
let mut content = String::new();
file.read_to_string(&mut content)?;
Ok(content)
}
/// Processes content with error propagation.
fn process_content(&self, content: &str) -> Result<String, FileError> {
if content.is_empty() {
return Err(FileError::InvalidFormat("Content is empty".to_string()));
}
if content.len() > 10000 {
return Err(FileError::InvalidFormat("Content too long".to_string()));
}
Ok(content.to_uppercase())
}
/// Processes multiple files with error propagation.
pub fn process_multiple_files(&mut self, filenames: &[&str]) -> Result<Vec<usize>, FileError> {
let mut results = Vec::new();
for filename in filenames {
let result = self.process_file(filename)?;
results.push(result);
}
Ok(results)
}
}
/// Demonstrates error propagation with anyhow.
pub struct AnyhowErrorDemo {
data: Vec<String>,
}
impl AnyhowErrorDemo {
pub fn new() -> Self {
Self { data: Vec::new() }
}
/// Processes data with anyhow error handling.
pub fn process_data(&mut self, input: &str) -> AnyhowResult<usize> {
let processed = self.validate_input(input)
.context("Failed to validate input")?;
let result = self.process_content(&processed)
.context("Failed to process content")?;
self.data.push(result);
Ok(self.data.len())
}
/// Validates input with context.
fn validate_input(&self, input: &str) -> AnyhowResult<String> {
if input.is_empty() {
anyhow::bail!("Input cannot be empty");
}
if input.len() > 100 {
anyhow::bail!("Input too long: {} characters", input.len());
}
Ok(input.to_string())
}
/// Processes content with context.
fn process_content(&self, content: &str) -> AnyhowResult<String> {
if content.contains("error") {
anyhow::bail!("Content contains error keyword");
}
Ok(content.to_uppercase())
}
/// Processes data with error chaining.
pub fn process_data_with_chain(&mut self, input: &str) -> AnyhowResult<usize> {
let processed = self.process_data(input)?;
Ok(processed)
}
}
/// Demonstrates error propagation with custom error types.
pub struct CustomErrorPropagationDemo {
data: Vec<String>,
}
impl CustomErrorPropagationDemo {
pub fn new() -> Self {
Self { data: Vec::new() }
}
/// Processes data with custom error propagation.
pub fn process_data(&mut self, input: &str) -> Result<usize, FileError> {
let validated = self.validate_input(input)?;
let processed = self.process_content(&validated)?;
self.data.push(processed);
Ok(self.data.len())
}
/// Validates input with custom error.
fn validate_input(&self, input: &str) -> Result<String, FileError> {
if input.is_empty() {
return Err(FileError::InvalidFormat("Input cannot be empty".to_string()));
}
if input.len() > 100 {
return Err(FileError::InvalidFormat("Input too long".to_string()));
}
Ok(input.to_string())
}
/// Processes content with custom error.
fn process_content(&self, content: &str) -> Result<String, FileError> {
if content.contains("error") {
return Err(FileError::InvalidFormat("Content contains error keyword".to_string()));
}
Ok(content.to_uppercase())
}
/// Processes data with error mapping.
pub fn process_data_with_mapping(&mut self, input: &str) -> Result<usize, String> {
self.process_data(input)
.map_err(|e| format!("Failed to process data: {}", e))
}
}
/// Demonstrates error propagation with async code.
pub struct AsyncErrorPropagationDemo {
data: Vec<String>,
}
impl AsyncErrorPropagationDemo {
pub fn new() -> Self {
Self { data: Vec::new() }
}
/// Processes data asynchronously with error propagation.
pub async fn process_data_async(&mut self, input: &str) -> Result<usize, FileError> {
let validated = self.validate_input_async(input).await?;
let processed = self.process_content_async(&validated).await?;
self.data.push(processed);
Ok(self.data.len())
}
/// Validates input asynchronously.
async fn validate_input_async(&self, input: &str) -> Result<String, FileError> {
// Simulate async work
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
if input.is_empty() {
return Err(FileError::InvalidFormat("Input cannot be empty".to_string()));
}
if input.len() > 100 {
return Err(FileError::InvalidFormat("Input too long".to_string()));
}
Ok(input.to_string())
}
/// Processes content asynchronously.
async fn process_content_async(&self, content: &str) -> Result<String, FileError> {
// Simulate async work
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
if content.contains("error") {
return Err(FileError::InvalidFormat("Content contains error keyword".to_string()));
}
Ok(content.to_uppercase())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_propagation_demo() {
let mut demo = ErrorPropagationDemo::new();
// This will fail because the file doesn't exist
let result = demo.process_file("nonexistent.txt");
assert!(result.is_err());
match result.unwrap_err() {
FileError::NotFound { path } => assert_eq!(path, "nonexistent.txt"),
_ => panic!("Expected NotFound error"),
}
}
#[test]
fn test_anyhow_error_demo() {
let mut demo = AnyhowErrorDemo::new();
let result = demo.process_data("hello");
assert!(result.is_ok());
assert_eq!(result.unwrap(), 1);
let result = demo.process_data("");
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Input cannot be empty"));
}
#[test]
fn test_custom_error_propagation_demo() {
let mut demo = CustomErrorPropagationDemo::new();
let result = demo.process_data("hello");
assert!(result.is_ok());
assert_eq!(result.unwrap(), 1);
let result = demo.process_data("");
assert!(result.is_err());
match result.unwrap_err() {
FileError::InvalidFormat(msg) => assert_eq!(msg, "Input cannot be empty"),
_ => panic!("Expected InvalidFormat error"),
}
}
#[tokio::test]
async fn test_async_error_propagation_demo() {
let mut demo = AsyncErrorPropagationDemo::new();
let result = demo.process_data_async("hello").await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), 1);
let result = demo.process_data_async("").await;
assert!(result.is_err());
match result.unwrap_err() {
FileError::InvalidFormat(msg) => assert_eq!(msg, "Input cannot be empty"),
_ => panic!("Expected InvalidFormat error"),
}
}
}
TL;DR Runbook¶
Quick Start¶
// 1. Basic error handling
use std::fs::File;
use std::io::{self, Read};
fn read_file(filename: &str) -> Result<String, io::Error> {
let mut file = File::open(filename)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
// 2. Custom error types
use thiserror::Error;
#[derive(Error, Debug)]
pub enum MyError {
#[error("Validation failed: {0}")]
Validation(String),
#[error("IO error: {0}")]
Io(#[from] io::Error),
}
// 3. Error propagation
fn process_data(input: &str) -> Result<String, MyError> {
let validated = validate_input(input)?;
let processed = process_content(&validated)?;
Ok(processed)
}
// 4. Error context
use anyhow::{Context, Result};
fn process_file(filename: &str) -> Result<String> {
let content = std::fs::read_to_string(filename)
.context("Failed to read file")?;
Ok(content)
}
Essential Patterns¶
// Complete error handling setup
pub fn setup_rust_error_handling() {
// 1. Result and Option patterns
// 2. Custom error types
// 3. Error propagation
// 4. Error context
// 5. Error recovery
// 6. Error mapping
// 7. Error chaining
// 8. Error testing
println!("Rust error handling setup complete!");
}
This guide provides the complete machinery for Rust error handling. Each pattern includes implementation examples, error strategies, and real-world usage patterns for enterprise error management.