All articles
Rust Error Handling In Practice
Error Handling

Rust Error Handling In Practice

Rust's error handling is marked by its capability to handle errors at compile-time without throwing exceptions. This article will delve…

By Luis SoaresMay 24, 2023Original on Medium

Rust's error handling is marked by its capability to handle errors at compile-time without throwing exceptions. This article will delve into the basics of error handling in Rust, offering examples and best practices to guide you on your journey.

Types of Errors

In Rust, errors are classified into two primary types: recoverable and unrecoverable errors.

  1. Recoverable errors: These are errors that your program can recover from after it encounters them. For instance, if your program attempts to open a file that doesn't exist, it is a recoverable error because your program can then proceed to create the file. Rust represents recoverable errors with the Result<T, E> enum.
  2. Unrecoverable errors: These are errors that the program cannot recover from, causing it to stop execution. Examples include memory corruption or accessing a location beyond an array's boundaries. Rust represents unrecoverable errors with the panic! macro.

Unrecoverable Errors with panic!

When your program encounters an unrecoverable error, the panic! macro is used. This macro stops the program immediately, unwinding and cleaning up the stack. Here's a simple example:

fn main() {
    panic!("crash and burn");
}

When this program runs, it will print the message "crash and burn", unwind, clean up the stack, and then quit.

Recoverable Errors with Result<T, E>

For recoverable errors, Rust uses the Result<T, E> enum. This enum has two variants: Ok(value), which indicates that the operation was successful and contains the resulting value and Err(why)an explanation of why the operation failed.

For instance, here's a function that attempts to divide two numbers, returning a Result:

fn divide(numerator: f64, denominator: f64) -> Result<f64, &'static str> {
    if denominator == 0.0 {
        Err("Cannot divide by zero")
    } else {
        Ok(numerator / denominator)
    }
}

When calling this function, you can use pattern matching to handle the Result:

match divide(10.0, 0.0) {
    Ok(result) => println!("Result: {}", result),
    Err(err) => println!("Error: {}", err),
}

The Question Mark Operator

Rust has a convenient shorthand for propagating errors: the ? operator. If the value of the Result is Ok, the ? operator unwraps the value and gives it. If the value is Err, it returns from the function and gives the error.

Here's an example:

fn read_file(file_name: &str) -> Result<String, std::io::Error> {
    let mut f = File::open(file_name)?;
    let mut s = String::new();
    f.read_to_string(&mut s)?;
    Ok(s)
}

In this function, if File::open or f.read_to_string encounters an error, the function will immediately return that error. If not, the function will eventually replace the file's contents.

Best Practices

Here are some best practices for error handling in Rust:

  1. Use Result<T, E> for recoverable errors: If there's a chance your function might fail, it should return a Result. This allows the calling code to handle the failure case explicitly.
  2. Leverage the ? operator: Remember to use the operator for error propagation if you're writing a function that returns a Result. It makes your code cleaner and easier to read.
  3. Make use of the unwrap() or expect() methods sparingly: These methods will cause your program to panic if they're called on a Err variant. It's generally better to handle errors gracefully with match the ? operator.
  4. Don't panic!: Reserve panic! for situations when your code is in a state, it can't recover from. If there's any chance of recovery, return a Result instead.
  5. Customize error types: Rust allows you to define your own error types, which can give more meaningful error information. This can be particularly useful in more extensive programs and libraries.
  6. Handle all possible cases: When dealing with Result types, make sure your code handles both the Ok and Err variants. Rust's exhaustive pattern matching will remind you of this, but it's a good principle.

Here's an example of creating a custom error:

use std::fmt;

#[derive(Debug)]
struct MyError {
    details: String
}

impl MyError {
    fn new(msg: &str) -> MyError {
        MyError{details: msg.to_string()}
    }
}

impl fmt::Display for MyError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self.details)
    }
}

impl Error for MyError { fn description(&self) -> &str { &self.details } }


In this example, we create a new error type `MyError` that holds a `String`. We implement `fmt::Display` and `Error` for `MyError` so that it behaves like other errors.

Rust’s philosophy of “fearless concurrency” extends to its error-handling system. Rust enforces the handling of recoverable errors and allows the definition of custom error types, helping to produce more robust, reliable software.

### More Advanced Error Patterns

As your programs become complex, so will the errors you encounter and need to handle. Rust provides several advanced patterns and tools to help with this.

### Chaining Errors

It is often the case that an error in one function results from a series of other function calls that also return `Result`. For this situation, Rust provides a `map_err` function which can transform the error of a `Result` using a function you provide.

Consider the following example:

```rust
fn cook_pasta() -> Result<Pasta, CookingError> {
    let water = boil_water().map_err(|_| CookingError::BoilWaterError)?;
    let pasta = add_pasta(&water).map_err(|_| CookingError::AddPastaError)?;
    Ok(pasta)
}

Here, we're using map_err to transform any error from boil_water or add_pasta into a CookingError.

Using Error Traits

One of Rust's most powerful features is its trait system, which extends to its error handling. Specifically, Rust provides the std::error::Error trait, which you can implement on your error types.

If you're writing a library, consider providing your own custom error type so that users of your library can handle errors from your library specifically.

Here's an example of creating a custom error type with the Error trait:

use std::fmt;
use std::error::Error;

#[derive(Debug)]
pub struct CustomError {
    message: String,
}

impl CustomError {
    pub fn new(message: &str) -> CustomError {
        CustomError {
            message: message.to_string(),
        }
    }
}

impl fmt::Display for CustomError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self.message)
    }
}

impl Error for CustomError {}

This CustomError type implements the Error trait, which means it can be used with the ? operator and is interoperable with other kinds of errors.

Wrapping Errors

If you're dealing with many different kinds of errors in your function, you might want to use the anyhow or thiserror crate to make this easier.

The anyhow crate provides the anyhow! macro, which you can use to create an error of any type:

use anyhow::Result;

fn get_information() -> Result<()> {
    let data = std::fs::read_to_string("my_file.txt")?;
    // processing...
    Ok(())
}

The thiserror crate, on the other hand, is used for defining your own error types:

use thiserror::Error;

#[derive(Error, Debug)]
pub enum MyError {
    #[error("failed to read file")]
    ReadError,
    #[error("unknown error occurred")]
    Unknown,
}

Want to practice Rust hands-on?

Go beyond reading — solve interactive exercises with AI-powered code review on Rust Lab.