| name | rust-error-handling |
| user-invocable | false |
| description | Use when Rust error handling with Result, Option, custom errors, thiserror, and anyhow. Use when handling errors in Rust applications. |
| allowed-tools | ["Bash","Read"] |
Rust Error Handling
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:
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)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
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),
(e) => (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?;
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<, AppError> {
= std::fs::(path)?;
content.() {
(AppError::(path.()))
} {
(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)?;
if data.is_empty() {
return Err(DataError::NotFound(path.to_string()));
}
(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> {
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() {
let value = Some(42).unwrap();
let value = Some(42).expect("Value should be present");
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> {
let result = Ok(5).map(|x| x * 2);
let result = Err("error").map_err(|e| format!("Error: {}", e));
let result = Ok(5)
.and_then(|x| Ok(x * 2))
.and_then(|x| Ok(x + 1));
let result = Err("error")
.or_else(|_| Ok(42));
let value = Err("error").unwrap_or();
= ().(|_| );
(value)
}
Option Combinators
Using Option methods:
fn option_combinators() {
let result = Some(5).map(|x| x * 2);
let result = Some(5)
.and_then(|x| Some(x * 2))
.and_then(|x| Some(x + 1));
let result = None.or(Some(42));
let value = None.unwrap_or(42);
let result = Some(5).filter(|x| x > &3);
let result = Some(2).filter(|x| x > &);
: <, &> = ().();
}
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>) {
if let Ok(value) = result {
println!("Got value: {}", value);
}
if let Err(e) = result {
eprintln!("Error: {}", e);
}
}
Panic vs Result
When to panic:
fn get_element(index: usize) -> i32 {
let data = vec![1, 2, 3];
data[index]
}
fn safe_get_element(index: usize) -> Option<i32> {
let data = vec![1, 2, 3];
data.get(index).copied()
}
fn validate_config(value: i32) {
if value < 0 {
panic!("Config value must be positive, got {}", value);
}
}
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: ) <, > {
b == {
(::())
} {
(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
Resources