Better Programming

Advice for programmers.

Follow publication

How To Wrap Your Errors With Enums When Using Error-Stack

--

Static on a screen
Image by Michael Dziedzic on Unsplash

I am an intermediate Rust developer, and you may know me from my posts on subreddits like r/opensource or r/rust or from my various projects on GitHub.

Recently, we decided to do error handling and provide custom error messages for the errors related to each engine code present under the src/engines folder in one of my project’s, websurfx, when using error-stack. We wanted to use enums for it, and we found that the error-stack project provided no guide, tutorial, or examples. But one of our maintainers, @xffxff, provided a cool solution to this problem that helped me learn much, so I decided to share it with you all. You’ll learn how you can wrap errors with enums when using error-stack, so stick around until the end of the article.

Person sculpting wood
Image by cottonbro on Pexels

For this tutorial, I will be writing a simple scraping program to scrape the example.com webpage, and then we will write code to handle errors with enums based on it. Let’s dive straight into it.

Simple Scraping Program

Let’s first start by explaining the code I have written to scrape the More information href link in the example.com webpage. We will use it throughout the program to write the error-handling code for it. Here is the code:

//! The main module that fetches html code and scrapes the more information href link and displays
//! it on stdout.

use reqwest::header::{HeaderMap, CONTENT_TYPE, REFERER, USER_AGENT};
use scraper::{Html, Selector};
use std::{println, time::Duration};

fn main() -> Result<(), Box<dyn std::error::Error>> {
// A map that holds various request headers
let mut headers = HeaderMap::new();
headers.insert(
USER_AGENT,
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.0"
.parse()?, // --> (1)
);
headers.insert(REFERER, "https://google.com/".parse()?); // --> (1)
headers.insert(CONTENT_TYPE, "application/x-www-form-urlencoded".parse()?); // --> (1)

// A blocking request call that fetches the html text from the example.com site.
let html_results = reqwest::blocking::Client::new()
.get("https://example.com/")
.timeout(Duration::from_secs(30))
.headers(headers)
.send()? // --> (2)
.text()?; // --> (2)

// Parse the recieved html text to html for scraping.
let document = Html::parse_document(&html_results);

// Initialize a new selector for scraping more information href link.
let more_info_href_link_selector = Selector::parse("div>p>a")?; // --> (3)

// Scrape the more information href link.
let more_info_href_link = document
.select(&more_info_href_link_selector)
.next()
.unwrap()
.value()
.attr("href")
.unwrap();

// Print the more information link.
println!("More information link: {}", more_info_href_link);

Ok(())
}

In the Cargo.toml file, you will need to provide something like this under the dependencies section:

[dependencies]
scraper="0.16.0"
error-stack="0.3.1"
reqwest = {version="0.11.18",features=["blocking"]}

Now, I know the above code might seem intimidating and daunting, but you don’t need to focus on the implementation because it doesn’t matter what the code is. That’s why I have numbered the important parts for this tutorial.

You might be asking, what are those question marks in the numbered parts, and what do they do?

What is the Question Mark Operator

According to our favourite go-to resource, the Rust lang book:

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

Let me explain it briefly.

The question mark operator (?) in Rust, we can handle any operation that returns a result type with either a value or an error if something bad happens; it helps us handle these issues more gracefully. For example, if the operation is completed successfully, the execution of the rest of the function or program continues. Otherwise, it is propagated to the function that called it and stops the execution of the rest of the function.

The propagated error by the function can then be handled by matching it using the match statement. You can also use the if let syntax in the caller function. On the other hand, if the operation was done within the main function, and it fails, the rest of the code is never executed, and the error is then propagated to the standard output (stdout) as usual. It will get displayed on stdout.

For example, take this Rust code:

fn main() {
match caller("4", "6") {
Ok(sum) => println!("Sum is: {}", sum),
Err(error) => println!("{}", error),
}
}

fn sum_numbers_from_string(
number_x_as_string: &str,
number_y_as_string: &str,
) -> Result<u32, Box<dyn std::error::Error>> {
let number_x: u32 = number_x_as_string.parse()?;
let number_y: u32 = number_y_as_string.parse()?;

println!("This code is being executed!! and the code below will also be executed!! :)");

Ok(number_x + number_y)
}

Here, you can see the main function calls the function sum_numbers_from_string, which returns a Result type. If the code with the question mark operator were to error in the sum_numbers_from_string function, then the execution will stop there. The code below it, including the println!() statement will never be executed.

A ParseError will be propagated to the main function, which will then be matched over by the match statement and the Err handle will be executed.

Now, let’s take another example by placing the operations with the question mark operator in the main function. The code for this will look something like this:

fn main() -> Result<(), Box<dyn std::error::Error>> {
let number_x: u32 = "4".parse()?;
let number_y: u32 = "6".parse()?;

println!("This code is being executed!! and the code below will also be executed!! :)");

println!("Sum is: {}", number_x + number_y);

Ok(())
}

Now, if the above code were to be executed and the operation with the question mark operator were to fail, then, as usual, the program will stop executing, and the error will be propagated to stdout and printed on the terminal.

Note: I know I have missed a lot of finer details, but I have done it on purpose for the sake of understanding and simplicity. If you wish to learn more in depth, I would recommend reading this blog post.

Two women looking at code on a laptop
“Code analyzed and explained” | Image by Christina Morillo from Pexels

Writing Code to Handle Errors with Enums

Before we start, let’s go briefly over what each operation with question mark operator in each numbered part returns.

For the first numbered part, the operation gives a Result type that’s something like this — Result<HeaderValue, InvalidHeaderValue>. As you now know, if this operation fails the InvalidHeaderValue will be propagated by the main function to the stdout and will be displayed on it. Similarly, the second and third parts return Result types Result<String, ReqwestError> and Result<Selector, SelectorErrorKind>, respectively.

Now, as we know what each part returns, we can start writing code to wrap these errors with enums when using the error-stack crate. First, we will start by writing the error enum. Let's call it ScraperError.

After that, we will need to implement two traits on our error enum, Display and Context. The code looks something like this:

#[derive(Debug)]
enum ScraperError {
InvalidHeaderMapValue,
RequestError,
SelectorError,
}

Then we will need to implement two traits on our error enum the Display and Context traits the code for which looks something like this:

impl fmt::Display for ScraperError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ScraperError::InvalidHeaderMapValue => {
write!(f, "Invalid header map value provided")
}
ScraperError::RequestError => {
write!(f, "Error occurred while requesting data from the webpage")
}
ScraperError::SelectorError => {
write!(f, "An error occured while initializing new Selector")
}
}
}
}

impl Context for ScraperError {}

By implementing the Display trait, we provide each error type that will be encountered with an appropriate error message, and with the implementation of Context trait, we give the error enum the ability to be converted into a Report type. Otherwise, if this is not implemented, the program results in a compile-time error stating that the following cannot be converted into a Report type.

Now, we will need to replace each Question mark operator and change the return type of the main function to Result<(), ScraperError> . So the code will look something like this:

fn main() -> Result<(), ScraperError> {
// A map that holds various request headers
let mut headers = HeaderMap::new();
headers.insert(
USER_AGENT,
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.0"
.parse()
.into_report()
.change_context(ScraperError::InvalidHeaderMapValue)?, // --> (1)
);
headers.insert(
REFERER,
"https://google.com/"
.parse()
.into_report()
.change_context(ScraperError::InvalidHeaderMapValue)?,
); // --> (1)
headers.insert(
CONTENT_TYPE,
"application/x-www-form-urlencoded"
.parse()
.into_report()
.change_context(ScraperError::InvalidHeaderMapValue)?,
); // --> (1)

// A blocking request call that fetches the html text from the example.com site.
let html_results = reqwest::blocking::Client::new()
.get("https://example.com/")
.timeout(Duration::from_secs(30))
.headers(headers)
.send()
.into_report()
.change_context(ScraperError::RequestError)? // --> (2)
.text()
.into_report()
.change_context(ScraperError::RequestError)?; // --> (2)

// Parse the recieved html text to html for scraping.
let document = Html::parse_document(&html_results);

// Initialize a new selector for scraping more information href link.
let more_info_href_link_selector = Selector::parse("div>p>a$")
.into_report()
.change_context(ScraperError::SelectorError)?; // --> (3)

// Scrape the more information href link.
let more_info_href_link = document
.select(&more_info_href_link_selector)
.next()
.unwrap()
.value()
.attr("href")
.unwrap();

// Print the more information link.
println!("More information link: {}", more_info_href_link);

Ok(())
}

Putting it all together, the code looks like this:

//! The main module that fetches html code and scrapes the more information href link and displays
//! it on stdout.

use core::fmt;
use error_stack::{Context, IntoReport, Result, ResultExt};
use reqwest::header::{HeaderMap, CONTENT_TYPE, REFERER, USER_AGENT};
use scraper::{Html, Selector};
use std::{println, time::Duration};

#[derive(Debug)]
enum ScraperError {
InvalidHeaderMapValue,
RequestError,
SelectorError,
}

impl fmt::Display for ScraperError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ScraperError::InvalidHeaderMapValue => {
write!(f, "Invalid header map value provided")
}
ScraperError::RequestError => {
write!(f, "Error occurred while requesting data from the webpage")
}
ScraperError::SelectorError => {
write!(f, "An error occured while initializing new Selector")
}
}
}
}

impl Context for ScraperError {}

fn main() -> Result<(), ScraperError> {
// A map that holds various request headers
let mut headers = HeaderMap::new();
headers.insert(
USER_AGENT,
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.0"
.parse()
.into_report()
.change_context(ScraperError::InvalidHeaderMapValue)?, // --> (1)
);
headers.insert(
REFERER,
"https://google.com/"
.parse()
.into_report()
.change_context(ScraperError::InvalidHeaderMapValue)?,
); // --> (1)
headers.insert(
CONTENT_TYPE,
"application/x-www-form-urlencoded"
.parse()
.into_report()
.change_context(ScraperError::InvalidHeaderMapValue)?,
); // --> (1)

// A blocking request call that fetches the html text from the example.com site.
let html_results = reqwest::blocking::Client::new()
.get("https://example.com/")
.timeout(Duration::from_secs(30))
.headers(headers)
.send()
.into_report()
.change_context(ScraperError::RequestError)? // --> (2)
.text()
.into_report()
.change_context(ScraperError::RequestError)?; // --> (2)

// Parse the recieved html text to html for scraping.
let document = Html::parse_document(&html_results);

// Initialize a new selector for scraping more information href link.
let more_info_href_link_selector = Selector::parse("div>p>a$")
.into_report()
.change_context(ScraperError::SelectorError)?; // --> (3)

// Scrape the more information href link.
let more_info_href_link = document
.select(&more_info_href_link_selector)
.next()
.unwrap()
.value()
.attr("href")
.unwrap();

// Print the more information link.
println!("More information link: {}", more_info_href_link);

Ok(())
}

But don’t be excited just yet because when you run the above code, it will throw a scary compile-time error as follows:

error[E0599]: the method `into_report` exists for enum `Result<Selector, SelectorErrorKind<'_>>`, but its trait bounds were not satisfied
|
::: /home/destruct/.cargo/registry/src/github.com-1ecc6299db9ec823/error-stack-0.3.1/src/report.rs:249:1
|
249 | pub struct Report<C> {
| -------------------- doesn't satisfy `_: From<SelectorErrorKind<'_>>`
|
::: /home/destruct/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:503:1
|
503 | pub enum Result<T, E> {
| --------------------- doesn't satisfy `_: IntoReport`
--> src/main.rs:77:10
|
76 | let more_info_href_link_selector = Selector::parse("div>p>a$")
| ________________________________________-
77 | | .into_report()
| |_________-^^^^^^^^^^^
|
= note: the following trait bounds were not satisfied:
`error_stack::Report<SelectorErrorKind<'_>>: From<SelectorErrorKind<'_>>`
which is required by `Result<Selector, SelectorErrorKind<'_>>: IntoReport`

For more information about this error, try `rustc --explain E0599`.
error: could not compile `error-stack-blog` due to previous error
Person looking through many documents
“Decoding the above error” | Image by cottonbro on Pexels

If you try to decode the error, it is very confusing and doesn’t really explain the real problem. The problem with our code is that the error returned from Selector::parse() operation in part three is not thread safe.

Fixing the Thread Safety Issue

To fix the above thread-safety error, we will need to map the error of the selector operation of part three to the error-stack Report type by constructing it. Also, we will add a custom error message that we want to print when this error is encountered.

“Fixing the code” | Image from Pexels

The code for mapping the error from the operation in part three will look like this:

let more_info_href_link_selector = Selector::parse("div>p>a$")
.map_err(|_| Report::new(ScraperError::SelectorError))
.attach_printable_lazy(|| "invalid CSS selector provided")?; // --> (3)

Putting it all together, the code looks like this:

//! The main module that fetches html code and scrapes the more information href link and displays
//! it on stdout.

use core::fmt;
use error_stack::{Context, IntoReport, Report, Result, ResultExt};
use reqwest::header::{HeaderMap, CONTENT_TYPE, REFERER, USER_AGENT};
use scraper::{Html, Selector};
use std::{println, time::Duration};

#[derive(Debug)]
enum ScraperError {
InvalidHeaderMapValue,
RequestError,
SelectorError,
}

impl fmt::Display for ScraperError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ScraperError::InvalidHeaderMapValue => {
write!(f, "Invalid header map value provided")
}
ScraperError::RequestError => {
write!(f, "Error occurred while requesting data from the webpage")
}
ScraperError::SelectorError => {
write!(f, "An error occured while initializing new Selector")
}
}
}
}

impl Context for ScraperError {}

fn main() -> Result<(), ScraperError> {
// A map that holds various request headers
let mut headers = HeaderMap::new();
headers.insert(
USER_AGENT,
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.0"
.parse()
.into_report()
.change_context(ScraperError::InvalidHeaderMapValue)?, // --> (1)
);
headers.insert(
REFERER,
"https://google.com/"
.parse()
.into_report()
.change_context(ScraperError::InvalidHeaderMapValue)?,
); // --> (1)
headers.insert(
CONTENT_TYPE,
"application/x-www-form-urlencoded"
.parse()
.into_report()
.change_context(ScraperError::InvalidHeaderMapValue)?,
); // --> (1)

// A blocking request call that fetches the html text from the example.com site.
let html_results = reqwest::blocking::Client::new()
.get("https://example.com/")
.timeout(Duration::from_secs(30))
.headers(headers)
.send()
.into_report()
.change_context(ScraperError::RequestError)? // --> (2)
.text()
.into_report()
.change_context(ScraperError::RequestError)?; // --> (2)

// Parse the recieved html text to html for scraping.
let document = Html::parse_document(&html_results);

// Initialize a new selector for scraping more information href link.
let more_info_href_link_selector = Selector::parse("div>p>a$")
.map_err(|_| Report::new(ScraperError::SelectorError))
.attach_printable_lazy(|| "invalid CSS selector provided")?; // --> (3)

// Scrape the more information href link.
let more_info_href_link = document
.select(&more_info_href_link_selector)
.next()
.unwrap()
.value()
.attr("href")
.unwrap();

// Print the more information link.
println!("More information link: {}", more_info_href_link);

Ok(())
}

Note: In the above code, I introduced a bug on purpose which allowed us to test whether the error-stack had been implemented successfully with the ScraperError enum.

Running the above code, you see that the code runs as expected. It throws a ScraperError, and it gives a beautiful error output with the first message using Display and the last message, which we had mapped to the error provided by the operation in the third part.

Error: An error occured while initializing new Selector
├╴at src/main.rs:77:22
╰╴invalid CSS selector provided

Finally, we’ve reached the end of this article. We have covered everything needed to wrap errors with enums when using error-stack crate.

Thanks for reading.

Want to Connect?

You can:

- Contact me on Reddit. My username is u/RevolutionaryAir1922.

- Message me on Discord. There, I'm known as neon_mmd or tag me on the
Rust Discord server.

If you want to geek out with us, you can join our project's Discord server.

--

--

Responses (2)

Write a response