Intermediate Level
Error Handling
Chapter 9: Error Handling ā
Rust's error handling is explicit and safe, using the Result and Option types instead of exceptions.
Unrecoverable Errors with panic!
Using panic!
fn main() {
panic!("crash and burn");
}Panic with Backtrace
fn main() {
let v = vec![1, 2, 3];
v[99]; // This will panic!
}To see backtraces:
RUST_BACKTRACE=1 cargo runRecoverable Errors with Result
Result Enum
enum Result<T, E> {
Ok(T),
Err(E),
}Basic Result Usage
use std::fs::File;
fn main() {
let f = File::open("hello.txt");
match f {
Ok(file) => {
println!("File opened successfully!");
// Use file here
}
Err(error) => {
println!("Failed to open file: {:?}", error);
}
}
}Matching on Different Errors
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Problem creating the file: {:?}", e),
},
other_error => panic!("Problem opening the file: {:?}", other_error),
},
};
}Shortcuts for Panic on Error
unwrap
use std::fs::File;
fn main() {
let f = File::open("hello.txt").unwrap();
}expect
use std::fs::File;
fn main() {
let f = File::open("hello.txt").expect("Failed to open hello.txt");
}Propagating Errors
Manual Propagation
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let f = File::open("hello.txt");
let mut f = match f {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut s = String::new();
match f.read_to_string(&mut s) {
Ok(_) => Ok(s),
Err(e) => Err(e),
}
}Using ? Operator
use std::fs::File;
use std::io::Read;
fn read_username_from_file() -> Result<String, std::io::Error> {
let mut f = File::open("hello.txt")?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}Chaining ? Operator
use std::fs::File;
use std::io::Read;
fn read_username_from_file() -> Result<String, std::io::Error> {
let mut s = String::new();
File::open("hello.txt")?.read_to_string(&mut s)?;
Ok(s)
}Where to Use ? Operator
In Functions That Return Result
fn main() {
let greeting_file = File::open("hello.txt")?; // ERROR: main doesn't return Result
}In Functions That Return Result (Correct)
use std::fs::File;
fn main() -> Result<(), std::io::Error> {
let greeting_file = File::open("hello.txt")?;
Ok(())
}Custom Error Types
Simple Custom Error
#[derive(Debug)]
enum MyError {
Io(std::io::Error),
Parse(std::num::ParseIntError),
}
impl std::fmt::Display for MyError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
MyError::Io(err) => write!(f, "IO error: {}", err),
MyError::Parse(err) => write!(f, "Parse error: {}", err),
}
}
}
impl std::error::Error for MyError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
MyError::Io(err) => Some(err),
MyError::Parse(err) => Some(err),
}
}
}
fn read_and_parse() -> Result<i32, MyError> {
let mut file = File::open("number.txt").map_err(MyError::Io)?;
let mut contents = String::new();
file.read_to_string(&mut contents).map_err(MyError::Io)?;
let number = contents.trim().parse::<i32>().map_err(MyError::Parse)?;
Ok(number)
}Using Box
use std::error::Error;
use std::fs::File;
use std::io::Read;
fn main() -> Result<(), Box<dyn Error>> {
let mut f = File::open("hello.txt")?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(())
}Option and Result Interoperability
Converting Option to Result
fn main() {
let opt = Some(5);
let res: Result<i32, &str> = opt.ok_or("Value was None");
println!("{:?}", res);
}Converting Result to Option
fn main() {
let res: Result<i32, &str> = Ok(5);
let opt = res.ok(); // Converts to Some(5)
let res: Result<i32, &str> = Err("error");
let opt = res.ok(); // Converts to None
println!("{:?}", opt);
}Practical Examples
Example 1: Safe Division
#[derive(Debug)]
enum DivisionError {
DivisionByZero,
Overflow,
}
fn safe_divide(dividend: f64, divisor: f64) -> Result<f64, DivisionError> {
if divisor == 0.0 {
Err(DivisionError::DivisionByZero)
} else {
let result = dividend / divisor;
if result.is_infinite() || result.is_nan() {
Err(DivisionError::Overflow)
} else {
Ok(result)
}
}
}
fn main() {
match safe_divide(10.0, 2.0) {
Ok(result) => println!("Result: {}", result),
Err(e) => println!("Error: {:?}", e),
}
match safe_divide(10.0, 0.0) {
Ok(result) => println!("Result: {}", result),
Err(e) => println!("Error: {:?}", e),
}
}Example 2: File Processing with Error Handling
use std::fs::File;
use std::io::{self, BufRead, BufReader};
fn read_lines(filename: &str) -> Result<Vec<String>, io::Error> {
let file = File::open(filename)?;
let reader = BufReader::new(file);
let mut lines = Vec::new();
for line in reader.lines() {
lines.push(line?);
}
Ok(lines)
}
fn process_file(filename: &str) -> Result<usize, io::Error> {
let lines = read_lines(filename)?;
Ok(lines.len())
}
fn main() {
match process_file("data.txt") {
Ok(count) => println!("File has {} lines", count),
Err(e) => println!("Error processing file: {}", e),
}
}Error Handling Best Practices
1. Use Result for Recoverable Errors
fn find_user(id: u32) -> Result<User, UserNotFoundError> {
// Implementation here
}2. Use Option for Missing Values
fn find_user_by_email(email: &str) -> Option<User> {
// Implementation here
}3. Don't Panic in Library Code
// Good: Return Result
fn parse_config(config_str: &str) -> Result<Config, ConfigError> {
// Implementation
}
// Bad: Panic in library code
fn parse_config(config_str: &str) -> Config {
if config_str.is_empty() {
panic!("Config string cannot be empty");
}
// Implementation
}4. Create Helpful Error Messages
fn divide(x: f64, y: f64) -> Result<f64, String> {
if y == 0.0 {
Err(format!("Cannot divide {} by zero", x))
} else {
Ok(x / y)
}
}Advanced Error Handling Patterns
Error Chaining
use std::error::Error;
use std::fmt;
#[derive(Debug)]
struct AppError {
message: String,
source: Option<Box<dyn Error + 'static>>,
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl Error for AppError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
self.source.as_ref().map(|e| e.as_ref() as &(dyn Error + 'static))
}
}
fn app_error<E: Error + 'static>(message: &str, source: E) -> AppError {
AppError {
message: message.to_string(),
source: Some(Box::new(source)),
}
}Key Takeaways
- ā
Use
panic!for unrecoverable errors - ā
Use
Result<T, E>for recoverable errors - ā
Use
Option<T>for optional values - ā
The
?operator propagates errors cleanly - ā Create custom error types for better error messages
- ā Don't panic in library code
- ā
Use
unwrapandexpectonly in prototyping or when you're certain
Ready for Chapter 10? ā Modules and Packages