A Comprehensive Guide for Handling Errors in Rust

Written by deliciousbounty | Published 2022/12/05
Tech Story Tags: rust | programming | error-handling | rust-programming-language | programming-languages | programming-tips | code-quality | tutorial

TLDRError Handling is a process that helps to identify, debug, and resolve errors that occur during the execution of a program. It helps to ensure the smooth functioning of the program by preventing errors from occurring and allowing the program to continue running in an optimal state. Error handling is a built-in [enum] in the Rust standard library. It has two variants Ok(T) and Err(E) in Rust. It can be used as a return type for a function that can encounter error situations.via the TL;DR App

The Rust community constantly discusses error handling. In this article, I will try to explain what it is, why, and how we should use it.

Purpose of Error Handling

Error handling is a process that helps to identify, debug, and resolve errors that occur during the execution of a program. It helps to ensure the smooth functioning of the program by preventing errors from occurring and allows the program to continue running in an optimal state. Error handling also allows users to be informed of any problems that may arise and take corrective action to prevent the errors from happening again in the future.

What is a Result?

A result is a built-in enum in the Rust standard library. It has two variants Ok(T) and Err(E).

Result should be used as a return type for a function that can encounter error situations. The Ok value is return in case of success or an Err value in case of an error.

Implementation of Result in a function.

What is Error Handling

Sometimes we use functions that can fail, for example when we call an endpoint from an API or search for a file. These types of functions can encounter errors (in our case the API is not reachable or the file does not exist). There are similar scenarios where we use Error Handling.

Explained Step by Step

  • A Result is the result of the read username from the file function. It follows that the function's returned value will either be an Ok that contains a String or an Err that contains an instance of io::Error.

There is a call to "File::open" inside of read username from the file, which returns a Result type.

  • It can return an Ok

  • It can return an Err

Then the code calls a match to check the result of the function and returns the value inside the ‘ok’ in the case the function was successful otherwise it returns the Error value.

In the second function read_to_string, the same principle is applied, but in this case, we did not use the keyword ‘return’ as you can see, and we finally return either an OK or an Err.

So you may ask: Do I have to write these Match blocks for every result type?

Don’t Fret! There’s a shortcut!

What is the Question Mark- Propagation Error?

According to the rust lang book:

The question mark operator (?) unwraps valid values or returns erroneous values, propagating them to the calling function. It is a unary postfix operator that can only be applied to the types Result and Option.

Let's explain it.

Question mark (?) in Rust is used to indicate a Result type. It is used to return an error value if the operation cannot be completed. For example, in our function that reads a file, it can return a Result type, where the question mark indicates that an error might be returned if the file cannot be read, or on the other hand the final result. In other words, used to short-circuit a chain of computations and return early if a condition is not met.

fn read_username_from_file() -> Result<String, io::Error> {
    let mut f = File::open("username.txt")?;
    let mut s = String::new();
    f.read_to_string(&mut s)?;
    Ok(s)
}

Every time you see a ?, that’s a possible early return from the function in case of Error, else , f will hold the file and handle the ‘Ok’ contained and then the execution of the function continues (similar to an unwrap function).

Why use crates to Handle errors?

The standard library does not provide all solutions for Error Handling. In fact, different errors may be returned by the same function, making it increasingly difficult to handle them precisely. Personal anecdote, in our company we developed Cherrybomb an API security tool written in Rust, and we need to re-write a good part of it to have better error handling.

For example:

Or the same message error can be displayed multiple times.

This is why we need to define our own custom Error enum.

Then our function will look like:

Customize Errors

Thiserror focuses on creating structured errors and has only one trait that can be used to define new errors:

Thiserror is an error-handling library for Rust that provides a powerful yet concise syntax to create custom error types.

In the cargo toml: [dependencies] thiserror = "1.0"

It allows developers to create custom error types and handlers without having to write a lot of boilerplate code.

Thanks to thiserror crate, we can customize our error messages.

It also provides features to automatically convert between custom error types and the standard error type. We will see it in the next Chapter with Dynamic Errors.

  • Create new errors through #[derive(Error)].
  • Enums, structs with named fields, tuple structs, and unit structs are all possible.
  • A Display img is generated for your error if you provide #[error("...")] messages on the struct or each variant of your enum and support string interpolation.

An example taken from docs.rs:

Dealing with Dynamic Errors

If you want to be able to use ‘?’, your Error type must implement the From trait for the error types of your dependencies. Your program or library may use many dependencies, each of which has its own error. You have two different structs of custom error, and we call a function that return one specific type. For example:

So when we call our main function that returns a ErrorA type, we encounter the following error:

So one of the solutions is to implement the trait From<ErrorB> for the struct ErrorA.

Our code looks like this now:

Another solution to this problem is to return dynamic errors. To handle dynamic errors in Rust, in the case of an Err value, you can use the box operator to return the error as a Box (a trait object of the Error trait). This allows the error type to be determined at runtime, rather than at compile time, making it easier to work with errors of different types.

The Box can then be used to store any type of Error, including those from external libraries or custom errors. The Box can then be used to propagate the Error up the call stack, allowing for the appropriate handling of the error at each stage.

Thiserror crate

To have cleaner code, let's use thiserror crate. The thiserror crate can help handle dynamic errors in Rust by allowing the user to define custom error types. It does this through the #[derive(thiserror::Error)] macro. This macro allows the user to define a custom error type with a specific set of parameters, such as an error code, a message, and the source of the error.

The user can then use this error type to return an appropriate error value in the event of a dynamic error. Additionally, the thiserror crate also provides several helpful methods, such as display_chain, which can be used to chain together multiple errors into a single error chain. In the following, we have created our error type ErrorB , then we used the From trait to convert ErrorB errors into our custom ErrorA error type. If a dynamic error occurs, you can create a new instance of your error type and return it to the caller. See function returns_error_a() in line 13.

Anyhow crate

anyhow was written by the same author, dtolnay, and released in the same week as thiserror. The anyhow can be used to return errors of any type that implement the std::error::Error trait and will display a nicely formatted error message if the program crashes. The most common way to use the crate is to wrap your code in a Result type. This type is an alias for the std::result::Result<T, E> type, and it allows you to handle success or failure cases separately.

When an error occurs, for example, you can use the context() method to provide more information about the error, or use the with_chain() method to chain multiple errors together. The anyhow crate provides several convenient macros to simplify the process of constructing and handling errors. These macros include the bail!() and try_with_context!()macros. The former can be used to quickly construct an error value, while the latter can be used to wrap a function call and automatically handle any errors that occur.

Comparison

The main difference between anyhow and the Thiserror crate in Rust is the way in which errors are handled. Anyhow allows for error handling using any type that implements the Error trait, whereas, Thiserror requires you to explicitly define the error types using macros.

Anyhow is an error-handling library for Rust that provides an easy way to convert errors into a uniform type. It allows the writing of concise and powerful error-handling code by automatically converting many different types of errors into a single, common type.

In conclusion, in Cherrybomb we choose to combine the two, in order to create a custom error type with thiserror and managed it with the anyhow crate.


Also published here.


Written by deliciousbounty | BLST finds broken logic in your API and maps it, with an easy-to-use & integrate platform.
Published by HackerNoon on 2022/12/05