Remember when your program crashed and you had no idea why? Somewhere in a stack trace, buried in someone else's library code, there was an exception. Maybe it was a null pointer. Maybe a file wasn't found. Maybe the network timed out. But you didn't write any code to handle it, because how could you have known?

Rust's error handling feels like extra work at first. Every function that can fail returns a Result. Every call site needs to handle it. It feels tedious.

Until you realize: this is the documentation. The compiler is telling you, right at the call site, exactly what can go wrong.

The Result Type

Here's the simplest possible error in most languages:

fn divide(a: f64, b: f64) -> f64 {
    a / b  // What if b is 0?
}

In Rust, this is a compile error — because you haven't handled the possibility of division by zero. The function looks like this:

fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err("Cannot divide by zero".to_string())
    } else {
        Ok(a / b)
    }
}

The Result type is just an enum with two variants:

enum Result<T, E> {
    Ok(T),    // Success — contains the value
    Err(E),  // Failure — contains the error
}

T is the type of the success value. E is the type of the error. In our case, we get a f64 on success, or a String on failure.

Handling Results

Now when you call divide, the compiler forces you to deal with both cases:

let result = divide(10.0, 2.0);

match result {
    Ok(value) => println!("Result: {}", value),
    Err(e) => println!("Error: {}", e),
}

This is verbose, but it's honest. You're not surprised at runtime because you've already accounted for every path.

The ? Operator (Your New Best Friend)

That match statement gets old fast. Enter the ? operator — Rust's way of propagating errors:

fn calculate() -> Result<f64, String> {
    let a = divide(10.0, 2.0)?;  // Returns early if error
    let b = divide(a, 5.0)?;     // Returns early if error
    Ok(b)
}

The ? operator means: "If this is an Err, return it early. If it's an Ok, unwrap it and continue."

It's syntactic sugar for:

let a = match divide(10.0, 2.0) {
    Ok(v) => v,
    Err(e) => return Err(e),
};

But much shorter. Much more readable.

Custom Error Types

Using String for errors is fine for quick scripts, but it doesn't scale. Real applications define custom error types:

#[derive(Debug)]
enum MathError {
    DivisionByZero,
    NegativeSquareRoot,
}

fn divide(a: f64, b: f64) -> Result<f64, MathError> {
    if b == 0.0 {
        Err(MathError::DivisionByZero)
    } else {
        Ok(a / b)
    }
}

This gives you type-safe errors. You can match on specific error types, convert between error types, and add data to errors.

The Anyhow Pattern

For applications (as opposed to libraries), many Rust developers use the anyhow crate:

use anyhow::{Result, Context};

fn read_config() -> Result<Config> {
    let content = std::fs::read_to_string("config.json")
        .context("Failed to read config file")?;
    
    let config: Config = serde_json::from_str(&content)
        .context("Failed to parse config JSON")?;
    
    Ok(config)
}

anyhow lets you add context to errors as they propagate. Instead of "parse error" you get "Failed to read config file: Failed to parse config JSON: ..."

Why This Is Better Than Exceptions

In languages with exceptions, a function can fail in ways the caller doesn't know about. The documentation might say "throws IOException" but that's easy to forget, and the compiler won't remind you.

In Rust:

The upfront cost is more typing. The long-term benefit is fewer "I didn't know this could fail" bugs.

Your Turn

Try writing a function that:

  1. Takes a filename as input
  2. Reads the file contents
  3. Parses it as a number
  4. Returns the number, or an appropriate error at each step

Use Result and the ? operator. Notice how the compiler guides you through handling each failure case.


Next up: Chapter 7 — Collections. Vectors, HashMaps, and when to use which.