Error Handling in Rust

Rust takes a unique and robust approach to error handling, grouping errors into two major categories: recoverable and unrecoverable errors. This distinction is enforced by the type system, helping you write more reliable code.

  • Recoverable errors are those that can be reasonably handled, such as a “file not found” error. Rust uses the Result<T, E> enum for these.
  • Unrecoverable errors are bugs or conditions from which it’s impossible to recover, like trying to access an array index that is out of bounds. Rust uses the panic! macro to handle these situations, which stops program execution.

Unrecoverable Errors with panic!

When the panic! macro is executed, your program will print a failure message, unwind and clean up the stack, and then quit. This is typically used for programming errors or unrecoverable states.

Example of panic!

You can call panic! directly in your code.

fn main() {
    // This will cause the program to crash with a message.
    panic!("Farewell, cruel world!");
}

A common unrecoverable error is accessing an invalid index in a collection.

fn main() {
    let v = vec![1, 2, 3];

    // This will panic because the index 99 is out of bounds.
    v[99]; 
}

Recoverable Errors with Result<T, E>

For errors that are expected and can be handled, Rust provides the Result<T, E> enum. It has two variants:

  • Ok(T): Contains the successfully computed value of type T.
  • Err(E): Contains an error value of type E.

Functions that might fail should return a Result.

Handling Result with match

The match expression is the most basic way to handle a Result.

use std::fs::File;

fn main() {
    let greeting_file_result = File::open("hello.txt");

    let greeting_file = match greeting_file_result {
        Ok(file) => file,
        Err(error) => panic!("Problem opening the file: {:?}", error),
    };
}

This pattern is so common that Result has helper methods to simplify it.

Shortcuts: unwrap and expect

  • unwrap(): If the Result is Ok, it returns the value inside. If it’s Err, it calls panic!. It’s a quick way to get a value but can lead to crashes if not used carefully.
  • expect(message): Similar to unwrap, but it lets you provide a custom panic message. This is often preferred over unwrap because it makes the reason for the panic clearer.
use std::fs::File;

fn main() {
    // This will panic with a default message if hello.txt doesn't exist.
    // let greeting_file = File::open("hello.txt").unwrap();

    // This will panic with a custom message.
    let greeting_file = File::open("hello.txt")
        .expect("hello.txt should be included in this project");
}

Propagating Errors with the ? Operator

When you’re writing a function that returns a Result, you often want to propagate errors from other functions you call. The ? operator provides a concise way to do this.

If the value of the Result is Ok, the value inside Ok will get returned from this expression, and the program will continue. If the value is Err, the Err will be returned from the whole function as if we had used the return keyword.

Example of the ? Operator

use std::fs::File;
use std::io::{self, Read};

// This function reads a username from a file.
// If File::open or read_to_string fails, the error is returned.
fn read_username_from_file() -> Result<String, io::Error> {
    let mut username_file = File::open("hello.txt")?;
    let mut username = String::new();
    username_file.read_to_string(&mut username)?;
    Ok(username)
}

fn main() {
    match read_username_from_file() {
        Ok(username) => println!("Username: {}", username),
        Err(e) => println!("Error reading username: {}", e),
    }
}

References