# Error Handling in Rust ## Result and Option Basics ```rust // Result: operation that can fail fn divide(a: f64, b: f64) -> Result { if b == 0.0 { Err("Division by zero".to_string()) } else { Ok(a / b) } } // Option: value that might be absent fn find_user(id: u64) -> Option { if id == 1 { Some(User { id, name: "Alice".to_string() }) } else { None } } // Using ? operator for propagation fn calculate(a: f64, b: f64, c: f64) -> Result { let x = divide(a, b)?; // Returns Err early if division fails let y = divide(x, c)?; Ok(y) } ``` ## Custom Error Types ```rust use std::fmt; // Manual error type #[derive(Debug)] enum AppError { NotFound(String), InvalidInput(String), DatabaseError(String), } impl fmt::Display for AppError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { AppError::NotFound(msg) => write!(f, "Not found: {}", msg), AppError::InvalidInput(msg) => write!(f, "Invalid input: {}", msg), AppError::DatabaseError(msg) => write!(f, "Database error: {}", msg), } } } impl std::error::Error for AppError {} // Usage fn get_user(id: u64) -> Result { if id == 0 { return Err(AppError::InvalidInput("ID cannot be zero".to_string())); } // ... fetch user Err(AppError::NotFound(format!("User {} not found", id))) } ``` ## Using thiserror ```rust use thiserror::Error; #[derive(Error, Debug)] enum DataError { #[error("Data not found: {0}")] NotFound(String), #[error("Invalid ID: {id}, reason: {reason}")] InvalidId { id: u64, reason: String }, #[error("IO error")] Io(#[from] std::io::Error), #[error("Parse error")] Parse(#[from] std::num::ParseIntError), #[error("Database error: {0}")] Database(#[from] sqlx::Error), } // Usage with automatic conversions fn read_config(path: &str) -> Result { let content = std::fs::read_to_string(path)?; // Auto-converts io::Error let port: u16 = content.parse()?; // Auto-converts ParseIntError Ok(Config { port }) } ``` ## Using anyhow for Applications ```rust use anyhow::{Result, Context, bail, ensure}; // Simple error handling for applications fn process_file(path: &str) -> Result<()> { let content = std::fs::read_to_string(path) .context(format!("Failed to read file: {}", path))?; ensure!(!content.is_empty(), "File is empty"); if content.len() > 1000 { bail!("File too large"); } // Process content... Ok(()) } // Adding context to errors fn main() -> Result<()> { process_file("config.txt") .context("Failed to process configuration")?; Ok(()) } ``` ## Option Combinators ```rust // map: transform Option to Option let num: Option = Some(5); let doubled = num.map(|n| n * 2); // Some(10) // and_then: chain operations let result = Some(5) .and_then(|n| if n > 0 { Some(n * 2) } else { None }) .and_then(|n| Some(n + 1)); // Some(11) // or: provide alternative let value = None.or(Some(42)); // Some(42) // unwrap_or: provide default let value = None.unwrap_or(42); // 42 // unwrap_or_else: compute default lazily let value = None.unwrap_or_else(|| expensive_computation()); // filter: conditional None let num = Some(5).filter(|&n| n > 10); // None // Pattern matching match find_user(1) { Some(user) => println!("Found: {}", user.name), None => println!("User not found"), } // if let for simple cases if let Some(user) = find_user(1) { println!("Found: {}", user.name); } ``` ## Result Combinators ```rust // map: transform Ok value let result: Result = Ok(5); let doubled = result.map(|n| n * 2); // Ok(10) // map_err: transform error let result: Result = Err("error"); let mapped = result.map_err(|e| e.to_uppercase()); // Err("ERROR") // and_then: chain fallible operations fn parse_then_double(s: &str) -> Result { s.parse::() .and_then(|n| Ok(n * 2)) } // or_else: provide alternative computation let result = Err("error").or_else(|_| Ok(42)); // Ok(42) // unwrap_or: provide default let value = Err("error").unwrap_or(42); // 42 // expect: unwrap with custom panic message let value = result.expect("Failed to parse number"); // Pattern matching match divide(10.0, 2.0) { Ok(result) => println!("Result: {}", result), Err(e) => eprintln!("Error: {}", e), } ``` ## Error Conversion and From Trait ```rust use std::io; use std::num::ParseIntError; #[derive(Debug)] enum MyError { Io(io::Error), Parse(ParseIntError), } impl From for MyError { fn from(err: io::Error) -> Self { MyError::Io(err) } } impl From for MyError { fn from(err: ParseIntError) -> Self { MyError::Parse(err) } } // Now ? operator works with automatic conversion fn read_and_parse(path: &str) -> Result { let content = std::fs::read_to_string(path)?; // io::Error -> MyError let number = content.trim().parse()?; // ParseIntError -> MyError Ok(number) } ``` ## Advanced Error Patterns ```rust // Multiple error sources with Box use std::error::Error; fn complex_operation() -> Result> { let file = std::fs::read_to_string("data.txt")?; let number: i32 = file.trim().parse()?; Ok(format!("Number: {}", number)) } // Error with backtrace (nightly) #[derive(Debug)] struct DetailedError { message: String, backtrace: std::backtrace::Backtrace, } impl DetailedError { fn new(message: impl Into) -> Self { Self { message: message.into(), backtrace: std::backtrace::Backtrace::capture(), } } } // Recoverable vs unrecoverable errors fn might_fail(value: i32) -> Result { if value < 0 { Err("Negative value".to_string()) // Recoverable } else if value > 1000 { panic!("Value too large!"); // Unrecoverable } else { Ok(value * 2) } } ``` ## Try Blocks (Nightly) ```rust #![feature(try_blocks)] // Try block for localized error handling let result: Result> = try { let file = std::fs::read_to_string("config.txt")?; let num: i32 = file.trim().parse()?; num * 2 }; ``` ## Error Context Pattern ```rust use thiserror::Error; #[derive(Error, Debug)] #[error("{message}")] struct ContextError { message: String, #[source] source: Option>, } impl ContextError { fn new(message: impl Into) -> Self { Self { message: message.into(), source: None, } } fn with_source(mut self, source: impl Error + Send + Sync + 'static) -> Self { self.source = Some(Box::new(source)); self } } // Extension trait for adding context trait Context { fn context(self, message: impl Into) -> Result; } impl Context for Result { fn context(self, message: impl Into) -> Result { self.map_err(|e| ContextError::new(message).with_source(e)) } } ``` ## Best Practices - Use Result for recoverable errors, panic! for unrecoverable bugs - Prefer ? operator over unwrap() in production code - Use expect() with descriptive messages instead of unwrap() - Use thiserror for libraries (structured errors) - Use anyhow for applications (simple error handling) - Implement std::error::Error trait for custom error types - Add context to errors as they propagate up the stack - Use #[from] in thiserror for automatic conversions - Document error conditions in function documentation - Use Option::ok_or() to convert Option to Result - Use Result::ok() to convert Result to Option (discarding error) - Avoid String as error type (use custom types instead) - Use ensure! and bail! from anyhow for cleaner checks - Log errors at boundaries, return them in library code