Lesson 11

Error Handling

Handle errors gracefully with Result<T, E>, the ? operator, and avoid panicking.

Result: Errors as Values

Rust doesn't have exceptions. Instead, functions that can fail return Result<T, E> -- an enum with two variants: Ok(value) for success and Err(error) for failure. The compiler forces you to handle both cases, so errors can't silently slip by.

enum Result<T, E> {
    Ok(T),
    Err(E),
}

Handling Results

You can handle a Result with match, unwrap() (panics on error -- avoid in production), unwrap_or(default), or expect("message"). For clean error propagation, the ? operator is your best friend -- it returns the error early if one occurs, or unwraps the success value.

The ? Operator

Place ? after any expression that returns a Result. If it's Ok, the value is extracted. If it's Err, the error is returned immediately from the enclosing function. This keeps your code clean and readable without hiding error paths.

Try it

use std::num::ParseIntError;

// A function that can fail -- returns Result
fn parse_and_double(s: &str) -> Result<i32, ParseIntError> {
    let n = s.parse::<i32>()?;  // ? returns early on error
    Ok(n * 2)
}

// Chaining multiple operations that can fail
fn parse_and_add(a: &str, b: &str) -> Result<i32, ParseIntError> {
    let x = a.parse::<i32>()?;
    let y = b.parse::<i32>()?;
    Ok(x + y)
}

fn main() {
    // --- Basic Result handling with match ---
    println!("--- Parsing Numbers ---");
    let good: Result<i32, _> = "42".parse();
    let bad: Result<i32, _> = "abc".parse();

    match good {
        Ok(n) => println!("  Parsed successfully: {}", n),
        Err(e) => println!("  Failed to parse: {}", e),
    }

    match bad {
        Ok(n) => println!("  Parsed successfully: {}", n),
        Err(e) => println!("  Failed to parse: {}", e),
    }

    // --- Using helper methods ---
    println!("\n--- Convenient Methods ---");

    // unwrap_or: provide a default on error
    let value = "not_a_number".parse::<i32>().unwrap_or(0);
    println!("  unwrap_or(0): {}", value);

    // unwrap_or_else: compute default lazily
    let value = "100".parse::<i32>().unwrap_or_else(|_| -1);
    println!("  Parsed '100': {}", value);

    // --- Using the ? operator ---
    println!("\n--- Using ? Operator ---");

    match parse_and_double("21") {
        Ok(result) => println!("  parse_and_double(\"21\") = {}", result),
        Err(e) => println!("  Error: {}", e),
    }

    match parse_and_double("oops") {
        Ok(result) => println!("  parse_and_double(\"oops\") = {}", result),
        Err(e) => println!("  Error: {}", e),
    }

    // --- Chaining results ---
    println!("\n--- Chaining Results ---");

    match parse_and_add("10", "20") {
        Ok(sum) => println!("  10 + 20 = {}", sum),
        Err(e) => println!("  Error: {}", e),
    }

    match parse_and_add("10", "nope") {
        Ok(sum) => println!("  10 + nope = {}", sum),
        Err(e) => println!("  Error: {}", e),
    }

    // --- Practical example: safe division ---
    println!("\n--- Safe Division ---");
    println!("  10 / 3 = {:?}", safe_divide(10.0, 3.0));
    println!("  10 / 0 = {:?}", safe_divide(10.0, 0.0));
}

fn safe_divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err(String::from("division by zero"))
    } else {
        Ok(a / b)
    }
}

Challenge

Write a function that takes a string, parses it as a number, and returns Result<String, ParseIntError> with a message like "The number is: 42". Use the ? operator.

Previous: Pattern Matching

Next: Collections