PHASE 1 ← Back to Course
5 / 17
🛡️

Error Handling

Handle errors gracefully with Result, Option, the ? operator, and custom error types.

1

panic! for Unrecoverable Errors

Rust requires you to acknowledge and handle the possibility of failure. This explicit approach prevents bugs where errors are silently ignored. Rust groups errors into two categories: recoverable and unrecoverable.

When something goes catastrophically wrong and there's no way to recover, use panic!. It terminates the program:

Rust
fn main() {
    panic!("crash and burn");  // Program terminates here
}

Panics often result from accessing out-of-bounds array indices or unwrapping None values:

Rust
fn main() {
    let v = vec![1, 2, 3];
    // This will panic - index out of bounds
    // println!("{}", v[99]);
    // This will panic - None value
    let option: Option<i32> = None;
    // let value = option.unwrap();  // Panic!
}
💡

When to Use panic!

Use panic! for truly unrecoverable situations. For most error cases, use Result<T, E> instead.

2

Result<T, E> for Recoverable Errors

The Result enum has two variants and is Rust's standard way to handle recoverable errors:

Rust
enum Result<T, E> {
    Ok(T),     // Operation succeeded with value T
    Err(E),    // Operation failed with error E
}

Basic Result Handling

Rust
use std::fs::File;
fn main() {
    let f = File::open("hello.txt");
    match f {
        Ok(file) => println!("File opened successfully"),
        Err(error) => println!("Error opening file: {:?}", error),
    }
}

Unwrap and Expect

unwrap() returns the Ok value or panics. expect() does the same but with a custom error message:

Rust
use std::fs::File;
fn main() {
    let f = File::open("hello.txt")
        .expect("Failed to open hello.txt");  // Panic with message if error
    // Or use unwrap (less informative)
    // let f = File::open("hello.txt").unwrap();
}
⚠️

Warning

Avoid unwrap() in production code. It will crash your program without a useful error message. Prefer expect() with a descriptive message, or handle the error explicitly.

3

Propagating Errors with ?

The ? operator simplifies error propagation. If a function returns an error, ? returns that error from the current function:

Rust
use std::fs::File;
use std::io;
use std::io::Read;
fn read_username_from_file() -> Result<String, io::Error> {
    let mut f = File::open("username.txt")?;  // Propagate error
    let mut s = String::new();
    f.read_to_string(&mut s)?;  // Propagate error
    Ok(s)
fn main() {
    match read_username_from_file() {
        Ok(username) => println!("Username: {}", username),
        Err(error) => println!("Error: {:?}", error),
    }
}

Chaining Results

Use methods like and_then and map to work with Results functionally:

Rust
fn parse_and_square(s: &str) -> Result<i32, std::num::ParseIntError> {
    s.parse::<i32>()
        .map(|n| n * n)  // Transform Ok value
fn main() {
    match parse_and_square("5") {
        Ok(result) => println!("Result: {}", result),  // Prints: 25
        Err(e) => println!("Error: {}", e),
    }
}

Key Insight

The ? operator is syntactic sugar that replaces verbose match blocks. It automatically converts the error type if the From trait is implemented, making error propagation clean and composable.

4

Custom Error Types

For complex applications, define custom error types:

Rust
use std::fmt;
struct CustomError {
    message: String,
impl fmt::Display for CustomError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Custom Error: {}", self.message)
    }
impl fmt::Debug for CustomError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{{ file: {}, line: {} }}", file!(), line!())
    }
fn main() {
    let error = CustomError {
        message: String::from("Something went wrong"),
    };
    println!("{}", error);    // Uses Display
    println!("{:?}", error);  // Uses Debug
}
5

The thiserror and anyhow Crates

For production code, use popular error handling crates. First, add them to your Cargo.toml:

TOML
[dependencies]
thiserror = "1.0"
anyhow = "1.0"

Using thiserror

thiserror makes it easy to define custom error types:

Rust
use thiserror::Error;
#[derive(Error, Debug)]
pub enum DataError {
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
    #[error("Parse error: {0}")]
    Parse(#[from] std::num::ParseIntError),
    #[error("Invalid data: {0}")]
    Invalid(String),
fn process_data(s: &str) -> Result<i32, DataError> {
    s.parse()
        .map_err(|_| DataError::Invalid(String::from("Not a number")))
fn main() {
    match process_data("not_a_number") {
        Ok(num) => println!("Parsed: {}", num),
        Err(e) => eprintln!("Error: {}", e),
    }
}

Using anyhow

anyhow provides flexible error handling for quick development:

Rust
use anyhow::{Result, Context};
use std::fs;
fn read_config(path: &str) -> Result<String> {
    let contents = fs::read_to_string(path)
        .context("Failed to read config file")?;
    Ok(contents)
fn main() {
    match read_config("config.txt") {
        Ok(config) => println!("Config: {}", config),
        Err(e) => eprintln!("Error: {}", e),
    }
}
💡

Tip

Use thiserror for libraries (structured, typed errors) and anyhow for applications (flexible, quick error handling). They complement each other well.

6

Practical Error Handling Pattern

Here's a practical example combining multiple concepts:

Rust
use std::fs::File;
use std::io::{self, Read};
fn read_and_parse_file(path: &str) -> Result<i32, Box<dyn std::error::Error>> {
    // Open file, propagating IO errors
    let mut file = File::open(path)?;
    // Read contents, propagating IO errors
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    // Parse to integer, propagating parse errors
    let number = contents.trim().parse::<i32>()?;
    Ok(number)
fn main() {
    match read_and_parse_file("number.txt") {
        Ok(num) => println!("Number: {}", num),
        Err(e) => {
            eprintln!("An error occurred: {}", e);
            std::process::exit(1);
        }
    }
}
7

Error Handling Best Practices

When designing error handling for your application:

Key Insight

Rust's error handling philosophy is "make invalid states unrepresentable." By encoding failure in the type system with Result and Option, the compiler catches missing error handling at compile time, not at runtime.

← Previous Chapter 5 of 17 Next →