ð rust-error-handling
Use when Rust error handling with Result, Option, custom errors, thiserror, and anyhow. Use when handling errors in Rust applications.
Overview
Master Rust's error handling mechanisms using Result, Option, custom error types, and popular error handling libraries for robust applications.
Result and Option
Result type for recoverable errors:
// Result<T, E> for operations that can fail
fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err(String::from("Division by zero"))
} else {
Ok(a / b)
}
}
fn main() {
match divide(10.0, 2.0) {
Ok(result) => println!("Result: {}", result),
Err(e) => println!("Error: {}", e),
}
}
Option type for optional values:
fn find_user(id: u32) -> Option<String> {
if id == 1 {
Some(String::from("Alice"))
} else {
None
}
}
fn main() {
match find_user(1) {
Some(name) => println!("Found: {}", name),
None => println!("User not found"),
}
}
Error Propagation with ?
Using ? operator:
use std::fs::File;
use std::io::{self, Read};
fn read_file(path: &str) -> Result<String, io::Error> {
let mut file = File::open(path)?; // Propagate error
let mut contents = String::new();
file.read_to_string(&mut contents)?; // Propagate error
Ok(contents)
}
// Equivalent without ? operator
fn read_file_explicit(path: &str) -> Result<String, io::Error> {
let mut file = match File::open(path) {
Ok(f) => f,
Err(e) => return Err(e),
};
let mut contents = String::new();
match file.read_to_string(&mut contents) {
Ok(_) => Ok(contents),
Err(e) => Err(e),
}
}
? with Option:
fn get_first_char(text: &str) -> Option<char> {
text.chars().next()
}
fn process_text(text: Option<&str>) -> Option<char> {
let t = text?; // Return None if text is None
get_first_char(t)
}
Custom Error Types
Simple custom error:
use std::fmt;
#[derive(Debug)]
struct ParseError {
message: String,
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Parse error: {}", self.message)
}
}
impl std::error::Error for ParseError {}
fn parse_number(s: &str) -> Result<i32, ParseError> {
s.parse().map_err(|_| ParseError {
message: format!("Failed to parse '{}'", s),
})
}
Enum-based error type:
use std::fmt;
use std::io;
#[derive(Debug)]
enum AppError {
Io(io::Error),
Parse(String),
NotFound(String),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
AppError::Io(e) => write!(f, "IO error: {}", e),
AppError::Parse(msg) => write!(f, "Parse error: {}", msg),
AppError::NotFound(item) => write!(f, "Not found: {}", item),
}
}
}
impl std::error::Error for AppError {}
impl From<io::Error> for AppError {
fn from(error: io::Error) -> Self {
AppError::Io(error)
}
}
fn process_file(path: &str) -> Result<String, AppError> {
let content = std::fs::read_to_string(path)?; // io::Error auto-converted
if content.is_empty() {
Err(AppError::NotFound(path.to_string()))
} else {
Ok(content)
}
}
thiserror Library
Install thiserror:
cargo add thiserror
Using thiserror for custom errors:
use thiserror::Error;
#[derive(Error, Debug)]
enum DataError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Parse error: {0}")]
Parse(String),
#[error("Validation failed: {field} is invalid")]
Validation { field: String },
#[error("Not found: {0}")]
NotFound(String),
}
fn validate_user(name: &str) -> Result<(), DataError> {
if name.is_empty() {
return Err(DataError::Validation {
field: "name".to_string(),
});
}
Ok(())
}
fn load_data(path: &str) -> Result<String, DataError> {
let data = std::fs::read_to_string(path)?; // Auto-converts io::Error
if data.is_empty() {
return Err(DataError::NotFound(path.to_string()));
}
Ok(data)
}
thiserror with source errors:
use thiserror::Error;
use std::io;
#[derive(Error, Debug)]
enum ConfigError {
#[error("Failed to read config file")]
ReadError {
#[source]
source: io::Error,
},
#[error("Invalid config format")]
ParseError {
#[source]
source: serde_json::Error,
},
}
anyhow Library
Install anyhow:
cargo add anyhow
Using anyhow for application errors:
use anyhow::{Result, Context, anyhow, bail};
fn read_config(path: &str) -> Result<String> {
let content = std::fs::read_to_string(path)
.context("Failed to read config file")?;
if content.is_empty() {
bail!("Config file is empty");
}
Ok(content)
}
fn process_data(value: i32) -> Result<i32> {
if value < 0 {
return Err(anyhow!("Value must be positive, got {}", value));
}
Ok(value * 2)
}
fn main() -> Result<()> {
let config = read_config("config.toml")
.context("Failed to load configuration")?;
let value = process_data(42)?;
println!("Value: {}", value);
Ok(())
}
anyhow with context chaining:
use anyhow::{Result, Context};
fn load_user(id: u32) -> Result<String> {
fetch_from_database(id)
.context("Database query failed")?
.parse()
.context(format!("Failed to parse user {}", id))
}
fn fetch_from_database(id: u32) -> Result<String> {
// Implementation
Ok(format!("user_{}", id))
}
Error Conversion
Converting between error types:
use std::io;
use std::num::ParseIntError;
enum AppError {
Io(io::Error),
Parse(ParseIntError),
}
impl From<io::Error> for AppError {
fn from(error: io::Error) -> Self {
AppError::Io(error)
}
}
impl From<ParseIntError> for AppError {
fn from(error: ParseIntError) -> Self {
AppError::Parse(error)
}
}
fn process() -> Result<i32, AppError> {
let content = std::fs::read_to_string("file.txt")?;
let number: i32 = content.trim().parse()?;
Ok(number)
}
unwrap and expect
When to use unwrap and expect:
fn unwrap_examples() {
// unwrap: panics with generic message
let value = Some(42).unwrap();
// expect: panics with custom message
let value = Some(42).expect("Value should be present");
// Only use in:
// 1. Tests
// 2. Prototypes
// 3. When you're certain it won't panic
// Better: handle the error
if let Some(value) = get_value() {
println!("{}", value);
}
}
fn get_value() -> Option<i32> {
Some(42)
}
Result Combinators
Using Result methods:
fn combinators() -> Result<i32, String> {
// map: transform Ok value
let result = Ok(5).map(|x| x * 2); // Ok(10)
// map_err: transform Err value
let result = Err("error").map_err(|e| format!("Error: {}", e));
// and_then (flatMap): chain operations
let result = Ok(5)
.and_then(|x| Ok(x * 2))
.and_then(|x| Ok(x + 1)); // Ok(11)
// or_else: provide alternative on error
let result = Err("error")
.or_else(|_| Ok(42)); // Ok(42)
// unwrap_or: provide default on error
let value = Err("error").unwrap_or(42); // 42
// unwrap_or_else: compute default on error
let value = Err("error").unwrap_or_else(|_| 42); // 42
Ok(value)
}
Option Combinators
Using Option methods:
fn option_combinators() {
// map: transform Some value
let result = Some(5).map(|x| x * 2); // Some(10)
// and_then (flatMap): chain operations
let result = Some(5)
.and_then(|x| Some(x * 2))
.and_then(|x| Some(x + 1)); // Some(11)
// or: provide alternative
let result = None.or(Some(42)); // Some(42)
// unwrap_or: provide default
let value = None.unwrap_or(42); // 42
// filter: keep only if predicate is true
let result = Some(5).filter(|x| x > &3); // Some(5)
let result = Some(2).filter(|x| x > &3); // None
// ok_or: convert Option to Result
let result: Result<i32, &str> = Some(5).ok_or("error"); // Ok(5)
}
Pattern Matching
Comprehensive error handling with match:
use std::fs::File;
use std::io::ErrorKind;
fn open_file(path: &str) -> File {
let file = match File::open(path) {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => {
match File::create(path) {
Ok(file) => file,
Err(e) => panic!("Failed to create file: {:?}", e),
}
}
ErrorKind::PermissionDenied => {
panic!("Permission denied: {}", path);
}
other_error => {
panic!("Failed to open file: {:?}", other_error);
}
},
};
file
}
if let for simple cases:
fn simple_match(result: Result<i32, String>) {
// Handle only the success case
if let Ok(value) = result {
println!("Got value: {}", value);
}
// Handle only the error case
if let Err(e) = result {
eprintln!("Error: {}", e);
}
}
Panic vs Result
When to panic:
// Panic for unrecoverable errors or bugs
fn get_element(index: usize) -> i32 {
let data = vec![1, 2, 3];
// Panic if index out of bounds (programmer error)
data[index]
}
// Use Result for expected errors
fn safe_get_element(index: usize) -> Option<i32> {
let data = vec![1, 2, 3];
data.get(index).copied()
}
// Custom panic messages
fn validate_config(value: i32) {
if value < 0 {
panic!("Config value must be positive, got {}", value);
}
}
// Conditional panic
fn debug_only_panic(condition: bool) {
debug_assert!(condition, "This only panics in debug builds");
}
Error Handling in Tests
Testing error conditions:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_success() {
let result = divide(10.0, 2.0);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 5.0);
}
#[test]
fn test_error() {
let result = divide(10.0, 0.0);
assert!(result.is_err());
}
#[test]
#[should_panic(expected = "Division by zero")]
fn test_panic() {
panic!("Division by zero");
}
#[test]
fn test_with_question_mark() -> Result<(), String> {
let result = divide(10.0, 2.0)?;
assert_eq!(result, 5.0);
Ok(())
}
}
fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err(String::from("Division by zero"))
} else {
Ok(a / b)
}
}
When to Use This Skill
Use rust-error-handling when you need to:
- Handle recoverable errors with Result
- Work with optional values using Option
- Create custom error types for your domain
- Use thiserror for library error types
- Use anyhow for application-level errors
- Propagate errors with the ? operator
- Convert between different error types
- Provide context to errors
- Implement comprehensive error handling
- Write robust error messages for debugging
Best Practices
- Use Result for recoverable errors, panic for unrecoverable ones
- Provide context with anyhow::Context in applications
- Use thiserror for library error types
- Implement Display and Error trait for custom errors
- Use ? operator for error propagation
- Avoid unwrap/expect in production code
- Return errors instead of logging and continuing
- Make error messages actionable and descriptive
- Use type system to prevent errors at compile time
- Document expected errors in function documentation
Common Pitfalls
- Overusing unwrap() leading to panics in production
- Not providing enough context in error messages
- Mixing panic and Result inconsistently
- Creating overly generic error types (String)
- Not implementing From for error conversions
- Ignoring errors with let _ = result
- Using Result when Option is more appropriate
- Not handling all error variants in match
- Creating error types that are hard to use
- Forgetting to propagate errors up the call stack