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 run

Recoverable 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 unwrap and expect only in prototyping or when you're certain

Ready for Chapter 10? → Modules and Packages

šŸ¦€ Rust Programming Tutorial

Learn from Zero to Advanced

Built with Next.js and Tailwind CSS • Open Source