No history yet

Error Handling

Handling the Unexpected

Programs don't always run perfectly. Network connections drop, files are missing, and users enter invalid data. A robust program anticipates these problems and handles them gracefully. In Rust, errors are divided into two main categories: recoverable and unrecoverable.

Unrecoverable errors are serious problems where it's not safe for the program to continue. Think of a critical file being missing at startup. In these cases, Rust will panic! and stop the program.

Recoverable errors are situations the program can handle. A user typing letters into a number field is an error, but one you can recover from by showing a message and asking for input again.

When to Panic

For unrecoverable errors, Rust provides the panic! macro. When this macro executes, your program will print an error message, unwind the stack, and exit. It's the programming equivalent of pulling the emergency brake.

fn main() {
    // This is an unrecoverable error.
    // The program will stop here.
    panic!("Emergency stop!");
}

You should only use panic! for true programming errors, like trying to access an array index that doesn't exist. For expected problems, like failed user input, you should use Rust's tools for recoverable errors.

Lesson image

Recoverable Errors

Rust doesn't have null values, which are a common source of bugs in other languages. Instead, it uses enums to represent a value that might be absent or might be an error.

You've already seen how to define your own enums. Rust's standard library comes with two incredibly useful ones for error handling: Option and Result.

Option

noun

An enum that represents a value that could be something or nothing. It has two variants: Some(T), which holds a value of type T, and None, which indicates the absence of a value.

Let's imagine a function that finds the first even number in a list. What if there are no even numbers? Instead of returning a confusing value like -1, it can return an Option.

fn find_first_even(numbers: &[i32]) -> Option<i32> {
    for &num in numbers {
        if num % 2 == 0 {
            return Some(num);
        }
    }
    None // No even number was found
}

fn main() {
    let nums1 = vec![1, 3, 5, 6, 7];
    let nums2 = vec![1, 3, 5, 7, 9];

    match find_first_even(&nums1) {
        Some(n) => println!("Found an even number: {}", n),
        None => println!("No even numbers found."),
    }

    match find_first_even(&nums2) {
        None => println!("Correctly found no even numbers."),
        Some(_) => println!("This shouldn't happen!"),
    }
}

Using match forces us to handle both the Some and None cases. The compiler ensures we can't forget about the possibility of an absent value, making our code more reliable.

The Result enum is similar, but it's used when an operation could either succeed and return a value, or fail and return an error. It has two variants: Ok(T) for success and Err(E) for an error.

// The definition of the Result enum
// T is the type of the success value
// E is the type of the error value
enum Result<T, E> {
    Ok(T),
    Err(E),
}

Let's try parsing a string into a number. This might fail if the string contains non-numeric characters. The parse method in Rust conveniently returns a Result.

fn main() {
    let num_str = "42";
    let not_num_str = "hello";

    let result1 = num_str.parse::<i32>();
    let result2 = not_num_str.parse::<i32>();

    match result1 {
        Ok(n) => println!("Successfully parsed: {}", n),
        Err(e) => println!("Error parsing: {}", e),
    }

    match result2 {
        Ok(n) => println!("This won't print: {}", n),
        Err(e) => println!("Failed to parse as expected: {}", e),
    }
}

Shortcuts for Handling Errors

Using match every time can be verbose. Rust provides some helpful shortcuts for working with Option and Result.

Sometimes you are absolutely certain that an Option will have a Some value, or a Result will be Ok. In these cases, you can use .unwrap() to get the value directly. But be careful! If you're wrong, your program will panic!.

let number = "100".parse::<i32>().unwrap(); // This is Ok(100), so we get 100

// This next line will panic!
let bad_parse = "nope".parse::<i32>().unwrap();

A slightly better version is .expect(), which does the same thing as unwrap() but lets you provide a custom panic message.

let bad_parse = "nope".parse::<i32>()
    .expect("Failed to parse the string!"); // Panics with this message

A more elegant way to handle errors is to propagate them. If a function you're writing calls another function that might fail, you can pass the error up to the code that called your function. The question mark operator, ?, makes this easy.

When you place a ? after a Result value, Rust does the following:

  1. If the value is Ok(T), it unwraps it and gives you T.
  2. If the value is Err(E), it immediately returns from the current function, passing the Err(E) along.
use std::num::ParseIntError;

// This function returns a Result. 
// It will return an Ok(i32) on success or a ParseIntError on failure.
fn parse_and_double(s: &str) -> Result<i32, ParseIntError> {
    let num = s.parse::<i32>()?; // If parse() fails, return the Err
    Ok(num * 2) // If it succeeds, double the number and wrap it in Ok
}

fn main() {
    println!("{:?}", parse_and_double("10"));   // Prints Ok(20)
    println!("{:?}", parse_and_double("twenty")); // Prints Err(...)
}

The ? operator is a clean way to chain operations that might fail, keeping your code focused on the success path while still correctly handling errors. You can only use the ? operator in functions that return a Result or Option themselves.

Quiz Questions 1/5

In Rust, when is it most appropriate to use the panic! macro?

Quiz Questions 2/5

A function needs to parse a string into an integer. The operation might fail if the string contains non-numeric characters. Which enum should this function return to best represent the outcome?

By forcing developers to confront potential errors, Rust's Option and Result enums help create remarkably stable and reliable software.