Skip to content

Rust - Error Handling

Rust takes error handling very seriously. Its design philosophy requires the programmer to acknowledge and handle potential failures, leading to exceptionally robust programs. Rust groups errors into two major categories:

CategoryDescriptionMechanism
Recoverable ErrorsErrors that can be reasonably expected and handled, such as a file not being found or invalid user input.The Result<T, E> enum
Unrecoverable ErrorsErrors that represent bugs or states from which it’s impossible to recover, like accessing an array index out of bounds.The panic! macro

Unlike many languages that use exceptions, Rust uses the return type system to encode error information, forcing you to deal with it at compile time.

The panic! macro signals that your program has reached an unrecoverable state. When it’s called, your program will print a failure message, unwind and clean up the stack, and then exit. It should be reserved for true programming errors.

fn main() {
// This is an example of an explicit panic.
// In a real application, this might be triggered by a critical invariant being violated.
panic!("A critical error occurred! Shutting down.");
// This line will never be reached.
println!("This will not be printed.");
}
thread 'main' panicked at 'A critical error occurred! Shutting down.', src/main.rs:4:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Many common operations, like accessing an invalid array index, will also cause a panic.

fn main() {
let numbers = [1, 2, 3];
numbers[99]; // This will panic because the index is out of bounds.
}

For errors that are expected to happen occasionally, Rust provides the Result<T, E> enum. It’s defined as:

enum Result<T, E> {
Ok(T), // Contains the success value
Err(E), // Contains the error value
}

A function that might fail will return a Result. If it succeeds, it returns Ok containing the result. If it fails, it returns Err containing information about the error.

The most fundamental way to handle a Result is with a match expression, which forces you to handle both the Ok and Err cases.

use std::fs::File;
fn main() {
let file_result = File::open("non_existent_file.txt");
let my_file = match file_result {
Ok(file) => {
println!("File opened successfully.");
file
},
Err(error) => {
// We gracefully handle the error instead of crashing.
panic!("Problem opening the file: {:?}", error);
},
};
println!("Program continues after handling the Result.");
}

Writing match expressions everywhere can be verbose. For functions that return a Result, Rust provides the powerful question mark (?) operator to simplify error propagation.

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

use std::fs::File;
use std::io::{self, Read};
// This function reads a username from a file.
// It returns a Result, so we can use the `?` operator inside it.
fn read_username_from_file() -> Result<String, io::Error> {
let mut username_file = File::open("hello.txt")?;
// If `File::open` fails, `?` causes the function to return the `Err` immediately.
let mut username = String::new();
username_file.read_to_string(&mut username)?;
// If `read_to_string` fails, `?` returns that error.
Ok(username)
// If everything succeeds, we wrap the result in `Ok`.
}
fn main() {
// In main, we handle the final result.
match read_username_from_file() {
Ok(username) => println!("Username: {}", username),
Err(e) => println!("Failed to read username: {}", e),
}
}

The ? operator significantly cleans up error-handling code and is the idiomatic way to propagate errors in Rust.

Shortcuts for Panicking on Error: unwrap and expect

Section titled “Shortcuts for Panicking on Error: unwrap and expect”

The Result<T, E> type has two helper methods that can be useful in specific situations, like in tests or quick prototypes. Use them with caution in production code.

  • unwrap(): If the Result is Ok, it returns the value inside. If it’s Err, it will panic! with a default message.
  • expect("message"): Works like unwrap(), but lets you provide a custom panic message.
use std::fs::File;
fn main() {
// Use `expect` when you are certain an operation should not fail,
// and if it does, it's a critical bug.
let greeting_file = File::open("hello.txt")
.expect("hello.txt should be included in this project");
// Using unwrap is less descriptive.
// let greeting_file = File::open("hello.txt").unwrap();
}
  • Prefer Result over panic!: For any error that is even remotely possible in normal operation, use Result.
  • Use the ? operator: Embrace the ? operator for clean and idiomatic error propagation within functions that return a Result.
  • Define custom error types: For libraries or large applications, create your own error enums to provide more specific and helpful error information.
  • Use unwrap and expect sparingly: Reserve them for tests, examples, or situations where a panic is the truly correct behavior if something goes wrong.