🦀 Rust Error Handling: Advanced Techniques
The Story of the Careful Messenger
Imagine you’re a messenger in a kingdom. Your job is to deliver packages between castles. But sometimes things go wrong:
- The bridge is broken 🌉
- The castle gate is locked 🔐
- The package is too heavy 📦
A good messenger doesn’t just give up. They pass the problem back to the person who sent them, with a clear note explaining what went wrong. That’s exactly what Rust’s advanced error handling does!
🚀 Propagating Errors
What Does “Propagating” Mean?
Propagating = Passing an error UP to whoever called you.
Think of it like a game of hot potato:
- You get a potato (an error)
- You can’t handle it yourself
- You pass it to your friend (the calling function)
Without Propagation (The Hard Way)
fn read_file() -> Result<String, io::Error> {
let file = File::open("hello.txt");
let mut file = match file {
Ok(f) => f,
Err(e) => return Err(e), // Pass error back
};
let mut contents = String::new();
match file.read_to_string(&mut contents) {
Ok(_) => Ok(contents),
Err(e) => Err(e), // Pass error back
}
}
This works, but it’s a lot of typing! 😓
❓ The Question Mark Operator
Magic with ?
Rust gives us a shortcut: the ? operator.
fn read_file() -> Result<String, io::Error> {
let mut file = File::open("hello.txt")?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
How Does ? Work?
graph TD A["Call function with ?"] --> B{Did it work?} B -->|Ok| C["Unwrap the value"] B -->|Err| D["Return error immediately"] C --> E["Continue running"]
Simple rule:
- ✅ If
Ok→ Unwrap and keep going - ❌ If
Err→ Stop and return that error
Even Shorter!
You can chain ? calls:
fn read_file() -> Result<String, io::Error> {
let mut s = String::new();
File::open("hello.txt")?
.read_to_string(&mut s)?;
Ok(s)
}
Or use one line:
fn read_file() -> Result<String, io::Error> {
fs::read_to_string("hello.txt")
}
🎨 Custom Error Types
Why Make Your Own Errors?
Imagine you’re running a pizza shop. Things can go wrong:
- No dough left 🍕
- Oven is broken 🔥
- Customer canceled 📞
Each problem is different! You want errors that describe exactly what happened.
Creating Custom Errors
#[derive(Debug)]
enum PizzaError {
NoDough,
OvenBroken,
OrderCanceled,
}
Using Custom Errors
fn make_pizza(dough: bool) -> Result<String, PizzaError> {
if !dough {
return Err(PizzaError::NoDough);
}
Ok(String::from("Delicious pizza!"))
}
Adding Helpful Messages
impl std::fmt::Display for PizzaError {
fn fmt(&self, f: &mut std::fmt::Formatter)
-> std::fmt::Result
{
match self {
PizzaError::NoDough =>
write!(f, "Out of dough!"),
PizzaError::OvenBroken =>
write!(f, "Oven needs repair!"),
PizzaError::OrderCanceled =>
write!(f, "Customer canceled!"),
}
}
}
🔄 The From Trait for Errors
The Problem
What if your function can have different types of errors?
// This tries to open a file AND parse a number
fn get_number() -> Result<i32, ???> {
let s = fs::read_to_string("num.txt")?; // io::Error
let n = s.trim().parse()?; // ParseIntError
Ok(n)
}
Two different error types! 😱
The Solution: From Trait
Create a custom error that can transform from other errors:
#[derive(Debug)]
enum MyError {
Io(io::Error),
Parse(ParseIntError),
}
impl From<io::Error> for MyError {
fn from(e: io::Error) -> Self {
MyError::Io(e)
}
}
impl From<ParseIntError> for MyError {
fn from(e: ParseIntError) -> Self {
MyError::Parse(e)
}
}
Now It Works!
fn get_number() -> Result<i32, MyError> {
let s = fs::read_to_string("num.txt")?;
let n = s.trim().parse()?;
Ok(n)
}
The ? operator automatically converts errors using From!
graph TD A["io::Error happens"] --> B["? operator sees it"] B --> C["Calls From::from"] C --> D["Converts to MyError::Io"] D --> E["Returns MyError"]
📋 The Error Trait
What Is the Error Trait?
The Error trait is like a membership card for errors. If your error type has this card, it can:
- Be used with
Box<dyn Error> - Work with error handling libraries
- Chain with other errors
Implementing Error Trait
use std::error::Error;
#[derive(Debug)]
struct GameError {
message: String,
}
impl std::fmt::Display for GameError {
fn fmt(&self, f: &mut std::fmt::Formatter)
-> std::fmt::Result
{
write!(f, "Game error: {}", self.message)
}
}
impl Error for GameError {}
Using Box<dyn Error>
When you want any error type:
fn do_stuff() -> Result<(), Box<dyn Error>> {
let file = File::open("data.txt")?;
let num: i32 = "42".parse()?;
Ok(())
}
This accepts:
io::Error✅ParseIntError✅- Your custom errors ✅
- Any error! ✅
🎯 Error Handling Patterns
Pattern 1: Early Return with ?
Best for: Simple functions that just pass errors up.
fn load_config() -> Result<Config, Error> {
let text = fs::read_to_string("config.toml")?;
let config = toml::from_str(&text)?;
Ok(config)
}
Pattern 2: Match for Recovery
Best for: When you can fix the problem.
fn get_setting() -> String {
match fs::read_to_string("settings.txt") {
Ok(s) => s,
Err(_) => String::from("default value"),
}
}
Pattern 3: Map Error
Best for: Adding context to errors.
fn open_config() -> Result<File, String> {
File::open("config.txt")
.map_err(|e| format!("Config error: {}", e))
}
Pattern 4: Unwrap with Context
Best for: Quick scripts or tests.
// Only use when you KNOW it won't fail!
let file = File::open("test.txt")
.expect("test.txt should exist");
Pattern 5: The anyhow Crate
Best for: Applications (not libraries).
use anyhow::{Context, Result};
fn main() -> Result<()> {
let config = fs::read_to_string("app.conf")
.context("Failed to read config")?;
Ok(())
}
Pattern 6: The thiserror Crate
Best for: Libraries with custom errors.
use thiserror::Error;
#[derive(Error, Debug)]
enum DataError {
#[error("File not found: {0}")]
NotFound(String),
#[error("Invalid format")]
InvalidFormat,
#[error(transparent)]
Io(#[from] io::Error),
}
🎮 Quick Decision Guide
graph TD A["Got an error?"] --> B{Can you fix it?} B -->|Yes| C["Use match to handle it"] B -->|No| D{Is it your library?} D -->|Yes| E["Create custom error type"] D -->|No| F["Use ? to propagate"] E --> G{Multiple error sources?} G -->|Yes| H["Implement From trait"] G -->|No| I["Simple enum is fine"]
🌟 Key Takeaways
| Concept | One-Line Summary |
|---|---|
| Propagating | Pass errors up with return Err(e) |
? Operator |
Shortcut for propagating errors |
| Custom Errors | Describe exactly what went wrong |
| From Trait | Convert between error types |
| Error Trait | Membership for “official” errors |
| Patterns | Match for recovery, ? for passing |
🚀 You Did It!
You now understand Rust’s advanced error handling! Remember:
?is your best friend for clean code- Custom errors tell users exactly what happened
- From trait lets errors transform automatically
- Error trait makes your errors “official”
- Choose the right pattern for each situation
Go forth and handle errors like a pro! 🦀✨
