Beyond the Basics: Result and Option
Let’s start with a quick refresher on Rust’s basic error handling types:
Result and Option Recap
// Result: for operations that can fail
fn divide(a: i32, b: i32) -> Result<i32, &'static str> {
if b == 0 {
Err("Division by zero")
} else {
Ok(a / b)
}
}
// Option: for values that might be absent
fn find_user(id: u64) -> Option<User> {
if id == 0 {
None
} else {
Some(User { id, name: "User".to_string() })
}
}
// Using Result and Option together
fn get_user_name(id: u64) -> Result<String, &'static str> {
find_user(id).map(|user| user.name).ok_or("User not found")
}
The ? Operator
fn process_data() -> Result<String, &'static str> {
let data = read_file("data.txt")?; // Returns early if Err
let processed = process_string(&data)?; // Returns early if Err
Ok(processed)
}
// Works with Option too
fn first_even_number(numbers: &[i32]) -> Option<i32> {
let first = numbers.first()?; // Returns None if empty
if first % 2 == 0 {
Some(*first)
} else {
None
}
}
Combining Result and Option
fn process_optional_data(data: Option<&str>) -> Result<String, &'static str> {
// Convert Option to Result
let data = data.ok_or("No data provided")?;
// Process the data
if data.is_empty() {
return Err("Data is empty");
}
Ok(data.to_uppercase())
}
// Using map and map_err for transformations
fn transform_data(input: Option<&str>) -> Result<i32, String> {
input
.ok_or("No input provided".to_string())
.map(|s| s.parse::<i32>())
.map_err(|e| format!("Invalid input: {}", e))?
.map_err(|e| format!("Parse error: {}", e))
}
Custom Error Types
For real-world applications, custom error types provide better organization and flexibility:
Basic Custom Error Type
#[derive(Debug)]
enum AppError {
IoError(std::io::Error),
ParseError(std::num::ParseIntError),
ValidationError(String),
NotFoundError(String),
}
impl std::fmt::Display for AppError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AppError::IoError(e) => write!(f, "I/O error: {}", e),
AppError::ParseError(e) => write!(f, "Parse error: {}", e),
AppError::ValidationError(msg) => write!(f, "Validation error: {}", msg),
AppError::NotFoundError(item) => write!(f, "Not found: {}", item),
}
}
}
impl std::error::Error for AppError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
AppError::IoError(e) => Some(e),
AppError::ParseError(e) => Some(e),
AppError::ValidationError(_) => None,
AppError::NotFoundError(_) => None,
}
}
}
// Implementing From for automatic conversions
impl From<std::io::Error> for AppError {
fn from(error: std::io::Error) -> Self {
AppError::IoError(error)
}
}
impl From<std::num::ParseIntError> for AppError {
fn from(error: std::num::ParseIntError) -> Self {
AppError::ParseError(error)
}
}
// Using the custom error type
fn read_and_parse(path: &str) -> Result<i32, AppError> {
let content = std::fs::read_to_string(path)?; // IoError converted automatically
let number = content.trim().parse::<i32>()?; // ParseError converted automatically
if number < 0 {
return Err(AppError::ValidationError("Number cannot be negative".to_string()));
}
Ok(number)
}
Using thiserror for Deriving Error Types
use thiserror::Error;
#[derive(Error, Debug)]
enum AppError {
#[error("I/O error: {0}")]
IoError(#[from] std::io::Error),
#[error("Parse error: {0}")]
ParseError(#[from] std::num::ParseIntError),
#[error("Validation error: {0}")]
ValidationError(String),
#[error("Not found: {0}")]
NotFoundError(String),
#[error("Database error: {0}")]
DatabaseError(#[from] DatabaseError),
}
#[derive(Error, Debug)]
enum DatabaseError {
#[error("Connection error: {0}")]
ConnectionError(String),
#[error("Query error: {0}")]
QueryError(String),
}
// Using the custom error type
fn process_user(user_id: u64) -> Result<User, AppError> {
let user = find_user(user_id)
.ok_or_else(|| AppError::NotFoundError(format!("User {}", user_id)))?;
if !is_valid_user(&user) {
return Err(AppError::ValidationError("Invalid user".to_string()));
}
Ok(user)
}