Rust error handling: Guide to avoid common Rust errors

This tutorial will help you solve common errors you may encounter using Rust.

Rust error handling: Common Rust errors and how to solve them

Rust is a modern programming language with a long history. Development started in the mid-2000s as a personal project by Mozilla Research employee Graydon Hoare. However, it wasn’t until 2015 that Rust 1.0, the first stable public release, was made available to users. In the time since it's quickly become popular for its emphasis on performance and memory safety.

Like any programming language, Rust is bound to throw you an error from time to time. Fortunately, it's equipped with a robust error-handling process that can help you diagnose and move past them quickly.

Types of Rust errors

There are two main types of errors in Rust: Recoverable and unrecoverable.

What’s a recoverable error in Rust?

A recoverable error in Rust is one that doesn't cause your program or app to abruptly stop running. When you receive a recoverable Rust error, you can instruct your program to retry the course of action that caused the error. Alternatively, you can specify in your code an alternate course of action to take when a certain recoverable error is encountered.

What’s an unrecoverable error in Rust?

An unrecoverable error in Rust is one that "breaks" the code and causes it to stop running or panic. Think of it like the “Game Over” screen in video games. When you receive an unrecoverable error or cause a panic, you must restart your program or app.

How to handle unrecoverable errors in Rust

Catch_unwind is one of several built-in ways to handle unrecoverable errors in a controlled manner that doesn't cause additional damage to your program or app when a panic happens — and it will happen.

The way catch_unwind works is by first invoking a closure and then capturing the cause of an unwinding panic if it occurs. When the function is executed, it will return Ok with the closure’s result if the closure doesn’t panic and return Err(cause) if it does.

When your program encounters a critical issue that it can't handle, a panic occurs, causing the program to terminate. You can cause a panic by directly causing the code to panic or by invoking the panic! macro. 

Calling the panic! macro will terminate the program and show you the panic message and where in the source code the panic occurred. You can then walk backward from the panic (using the backtrace of the function that made the panic! call) to figure out what went wrong. 

Unrecoverable Rust errors: Panic! example

Here's an example of how to cause a panic:

    fn displayNumbers() {
let x = [1, 2, 3];
println!(x[0]);
println!(x[1]);
println!(x[2]); // still in range
println!(x[3]); // panics due to index out of range
}
  

Because we attempt to print an index beyond the length of the array, the panic! output will display this: 

    thread 'displayNumbers' panicked at 'index out of bounds: The len is 3 but the index is 3', displayNumbers:5
  

The panic! macro can also be raised when something "impossible" happens like the connection to a socket getting suddenly terminated or a section of code should be impossible to reach (using something like todo! which throws a panic).

    Provide an example of diving by zero
fn divide(a: f32, b: f32) -> f32 {
    if b == 0 {
        panic!("cannot divide by zero")
    }
    return a / b;
}
Cannot divide by zero, therefore you should not define by zero as the case is not defined
  

If something doesn’t obey a business rule, it’s not an error — it’s an exception.

For things that are allowed to fail, there’s Result.

Recoverable Rust errors with Result

Result is another error handling feature in Rust that helps you handle recoverable errors as safely and efficiently as possible. It allows functions to return either a successful value of type T or an error value of type E. By explicitly defining these two possible outcomes, Rust enforces a rigorous error-handling process. By forcing developers to think about potential error scenarios as they're coding functions, Rust supports more reliable and resilient code. Developers must handle errors proactively rather than letting them propagate unchecked.

When you have a result, it doesn’t need to shock you. The Result type is more general; it holds the success or the failure. To get the type out of there, you need to define it. A Result is trying to have failure failure scenarios as a data type. 

Rust result type example

Here’s a code snippet that shows when you could use the Result type.

    fn check_if_odd(num: i32) -> Result {
    return if num % 2 != 0 {
        Ok(true)
    } else {
        Err("Number is not odd".to_string())
    }
}

fn main() {
    match check_if_odd(10) {
        Ok(res) => println!("{res}"),
        Err(err) => println!("{err}"),
    }
}
  

In the example above, the first function accepts an integer as a parameter and checks whether it is odd. If so, it returns a successful result, and if not, it returns an error along with a message explaining that the inputted integer is not odd. 

The second function, main(), is the one that uses the Result type. First, it passes a number to the check_if_odd() function. In this case, we're passing an even number — one that will fail the check. The function then uses the match feature to instruct the program what to do if the result passes and what to do if it receives an error.

If it passes, the program prints the number and a message confirming that it's odd. If it doesn't, the program prints the word "Error" followed by the specific error message passed back from the check_if_odd() function.

Common Rust errors

Here are a few of the most common errors you'll encounter in Rust.

Question Mark-Propagation error

A Question Mark-Propagation error in Rust occurs when the ? operator, used to streamline error handling, is used in a function in which the return type is a Result.

A Question Mark-Propagation error in Rust occurs when the ? operator, used to streamline error handling, is used in a function in which the return type is a Result.

The ? operator is designed to automatically handle error propagation by automatically unwrapping the Result, reducing the need for explicit error handling. The moment it’s an error, there’s an implied return with the error. 

Here's a simple example:

    fn get_file(filename:&str) -> Result {
let mut file = File::Open(filename)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents);
}
  

In the example above, the ? operator is used twice, both times in places where an error might be encountered in the file generation or conversion process. But because the method specifies a Result that takes into account both a successful result or an error, the ? operators are used appropriately. If you were to remove the Result but keep the ? operators, a Question Mark-Propagation Error would be thrown.

It’s important to note that for the ? operator to work, the errors need to be the same type. The point of the question mark is to unwrap the errors that share the same error type.

Compiler might complain if the compiler can’t compile — it isn’t an error, though. 

Customized errors

Customized errors in Rust allow you to create your own error types that correspond with specific error scenarios. With this practice, you can provide more relevant and specific error messages than the ones available in the standard Rust library. This can simplify the debugging process and make it easier and faster to identify and fix faulty code.

Anything can be an error type. For instance, enums are commonly used because they’re defined and discreet.

Here's an example of a customized error in Rust:

    enum CustomError {
    FileNotFound,
    PermissionDenied,
}
fn read_file(filename: &str) -> Result {
    let mut file = File::open(filename)?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    Ok(contents)
}
fn main() {
    match read_file("badfile.txt") {
        Ok(contents) => println!("File contents: {}", contents),
        Err(error) => match error {
            CustomError::FileNotFound => println("Error: File not found!"),
            CustomError::PermissionDenied => println("Error: Permission denied!"),
        },
    }
}
  

In the above snippet, the first function defines the CustomError as one that could represent either a FileNotFound or a PermissionDenied error.

The second function, read_file(), attempts to read a file and convert it to a string. The function calls that could result in an error use the ? operator to handle that possibility. Whether the error involves a file not found or a permission issue, it returns our user-defined CustomError as a result.

Here’s an example:

    fn read_content() -> Result<(), CustomError> {
    let content = read_file("badfile.txt")?;
   println!("File contents: {contents}");
    Ok(())
}
  

Common Rust errors and solutions

When programming in Rust, addressing common errors involves harnessing the language's powerful tools, such as the Result type for recoverable errors, error types for tailored error messages and  handling errors. There are also crates like thiserror that help with writing readable errors. A crate is the smallest amount of code that the Rust compiler will consider. 

With mechanisms like the ? operator and the match or unwrap methods, you can be even more concise  with handling any errors you might encounter in Rust.

Errors are a natural part of software development, no matter what language you're programming in. But you can minimize the impact they have on your code by making the best use of the tools Rust gives you to handle them.

Capital One  is always looking to the industry for ways to advance and improve our development process. As such, Capital One is exploring how Rust can be incorporated into our software engineering process. As we learn about these new technologies and the best practices in them, we want to share our learnings as well with the broader community.


Capital One Tech

Stories and ideas on development from the people who build it at Capital One.

Explore #LifeAtCapitalOne

Startup-like innovation with Fortune 100 capabilities.

Related Content

greyscale photo of laptop sitting on table with white screen with large black gear with black "R" in the middle
Article | June 25, 2018
microservices benefits
Article | August 3, 2020
Software Engineering
Article | August 3, 2023 |4 min read