Advanced Error Handling

Back

Loading concept...

🦀 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:

  1. ? is your best friend for clean code
  2. Custom errors tell users exactly what happened
  3. From trait lets errors transform automatically
  4. Error trait makes your errors “official”
  5. Choose the right pattern for each situation

Go forth and handle errors like a pro! 🦀✨

Loading story...

Story - Premium Content

Please sign in to view this story and start learning.

Upgrade to Premium to unlock full access to all stories.

Stay Tuned!

Story is coming soon.

Story Preview

Story - Premium Content

Please sign in to view this concept and start learning.

Upgrade to Premium to unlock full access to all content.