Rust community constantly discusses about error handling.. In this article I will try to explain what is it then 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?
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.
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 are using functions that can fail, for example calling an endpoint from an API or searching a file. These type of function can encounter errors (in our case the API is not reachable or the file is not existing).
There are similar scenarios where we are using Error Handling.
Explained Step by Step
- A Result is the result of the read username from 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 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 return the value inside the ok in the case the function was successful or return 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: On every result type I have to write all these Match block?
So hopefully there is 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 me 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 in 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 handle the Ok contained and execution of the function continues (similary to unwrap function).
Why use crates for Handle errors?
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 a better errors handling.
For example:
Or the same message error can be displayed multiples 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 errorsand 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.
Thank 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 Error.
- Create new errors through #[derive(Error)].
- Enums, structs with named fields, tuple structs, and unit structs are all possible.
- A Display impl is generated for your error if you provide #[error("...")] messages on the struct or each variant of your enum and support string interpolation.
Example taken from docs.rs:
Dealing Dynamic Errors handling
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 return a ErrorA
type, we encounter the following error:
So one of the solution 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 appropriate handling of the error at each stage.
Thiserror crate
In order to have a code clearer and soft 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 from 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 to write 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 combining the two, in order to create a custom error type with thiserror and managed it by the anyhow crate.
Top comments (2)
Very well written, thanks for sharing.
I get to understand from experts' discussion around, the error-handling in Rust could have been made more straightforward, instead of having so many libraries for conversion.
But, for what the mainstream programmers follow today, this blog is compact and very useful, IMO.
Thank you :)