Rust error propagation

Error Propagation means the code, that detects the problem, must propagate error information back to the caller function so that it can handle the problem. In

struct

Reading Time: 2 minutes

Error Propagation means the code, that detects the problem, must propagate error information back to the caller function so that it can handle the problem. In the real world, the practice of Error Propagation is necessary. One of the benefits is that your code will look cleaner by simply propagating error information back to the caller that can handle the error and another benefit is that your function doesn’t need extra code to propagate both the successful and unsuccessful cases back to the caller. I won’t go into details of this concept.

In this blog, I would explain how you can do this in Rust in different ways.

  1. Using match
use std::fs::File;
use std::io::Error;

fn open_file() -> Result<(), Error> {
    let file: Result<File, Error> = File::open("hello.txt");
    match file {
        Ok(_) => Ok(()),
        Err(e) => Err(e),
    }
}

fn main() {
    match open_file() {
        Ok(_) => println!("File is opened successfully!"),
        Err(e) => panic!(
            "Not able to open file. Here is the reason {:?}",
            e.to_string()
        ),
    }
}

If you are Rust developer, you would have known Result type, which is used to handle recoverable errors. In the above example, we are trying to open the file. File::open returns Result<File, Error>. We handle this value using match. If the file opened successfully, open_file will return Ok otherwise, pass the error value back to the caller function. Now caller function has to decide what to do with this error value. It can either create a new file hello.txt or show an error message.

2) Using try!

The above example can be written in a much shorter way. Let’s try with try. try! is a macro that returns Errs automatically in case of error otherwise it returns Ok variant. To use this macro, you have to use the raw-identifier syntax: r#try.

fn open_file() -> Result<(), Error> {
    let file = r#try!(File::open("hello.txt"));
    Ok(())
}

try! unwraps a result and early returns the function, if an error occurred.

3) Using ?operator

We can even shorten above code using ? operator. This operator was added to replace try! and it’s more idiomatic to use ? instead of try!.

fn open_file() -> Result<(), Error> {
    let file = File::open("hello.txt")?;
    Ok(())
}

As you can see this eliminates a lot of boilerplate and makes implementation simpler. I hope you enjoy reading this blog. Thanks!

Knoldus-blog-footer-image

Rust is an excellent alternative for programmers who want to discover why it is one of the most loved programming languages. Despite being one of the most enjoyable languages to work with, there are syntaxes that might be confusing or won’t make too much sense when you look at Rust code and you are new to working with this language, such as understanding the purpose of a question mark (?).

If you have a JavaScript background, you probably have seen the question mark to enable optional chaining. In other words, to optionally access properties of objects which values can be empty. Notice the team variable in the following example doesn’t have a players property. However, we attempt to access a method from the players property as if this property exists in the team object.

const team = {
    league: "La Liga",
    name: "Real Madrid"
};
team.players?.getTotalSalary();

Without using the question mark ?, the code will crash. However, the question mark enables optional chaining which prevents the code crashing, even if we attempt to access the method getTotalSalary() from a property that doesn’t exist in an object. While concepts such as chaining might work “kind of similar” in Rust, the question mark doesn’t work in the same way in Rust.

The question mark (?) operator in Rust is used as an error propagation alternative to functions that return Result or Option types. The ? operator is a shortcut as it reduces the amount of code needed to immediately return Err or None from the types Result<T, Err> or Option in a function.

After reading the definition of the question mark operator, it won’t make much sense if we don’t understand what we mean by error propagation in Rust. In this article, we will show a simple error propagation example as well as how the ? operator can reduce the amount of code but still maintain the same logic.

Understanding Error Propagation

Before we move forward with explaining the ? mark operator, do you know what error propagation is?

Error propagation is the process of “propagating“, spreading up or returning error information detected in the code generally triggered by a caller function to allow the caller function to properly handle the problem.

Let’s look at how error propagation works in code using the following example.

fn main() {
    let value = find_char_index_in_first_word(&"Learning the question mark", &'i');

    println!("What is the value {}", value.unwrap_or(1))
}


fn find_char_index_in_first_word(text: &str, char: &char) -> Option<usize> {
    let first_word =  match text.split(" ").next().is_some() == true {
        true => text.split(" ").next().unwrap(),
        false => return None
    };

    first_word.find(|x| &x == char)
}

Notice we have a simple helper function called find_char_index_in_first_word that returns the index of character found in the first word detected on a string literal.

Having said that, if the string literal is Learning the question mark and the character we want to detect is i, then the returned index value will be Some(5) because the first word of the string literal is Learning, which contains the character i in position 5 of the array of characters.

Hence, if we run the previous logic, there shouldn’t be any errors. However, the find_char_index_in_first_word could return Option::None because the return type definition is Option<>. Hence, the caller function is in charge of properly extracting the Option<> value.

To see an example of when the value returned is None, we can update the string literal passed ot the find_char_index_in_first_word to Hello World as the word Hello doesn’t have an i character.

fn main() {
    let value = find_char_index_in_first_word(&"Hello World", &'i');

    println!("What is the value {}", value.unwrap_or(1))
}

To extract the value of an Option<>, you can use the unwrap() method. However, this method can panic or trigger an error if the value attempting to extract is None. That’s why we use a safer alternative method called unwrap_or() in the main function to prevent the code from crashing as it uses instead a default value when value is None.

println!("What is the value {}", value.unwrap_or(1))

Hence, the printed value that you should see in the terminal is What is the value 1 after executing this code.

One aspect worth mentioning in this error propagation explanation is the fact that we are returning a value of type Option. Option is a type that can either be Some or None, as you will see in the definition below. However, none of these two possible values are errors themselves.

pub enum Option<T> {
    /// No value
    #[lang = "None"]
    #[stable(feature = "rust1", since = "1.0.0")]
    None,
    /// Some value `T`
    #[lang = "Some"]
    #[stable(feature = "rust1", since = "1.0.0")]
    Some(#[stable(feature = "rust1", since = "1.0.0")] T),
}

While it is correct that none of the values returned are errors, different to the Result<T, Err> type, where the Err type is clearly defined as the error, the option None is often used to report errors.

Remember how we use the find() method in the find_char_index_in_first_word function? To refresh our memory, let’s look at the definition of this method.

The find() method takes a closure that returns true or false. It applies this closure to each element of the iterator, and if any of them return true, then find() returns Some(element). If they all return false, it returns None.

Rust documentation

In other words, the find() method returns None as a way to say: There was an error. The value you attempted to find doesn’t exist. Hence, we are in some way or another propagating the error as the function find_char_index_in_first_word was meant to return a Some(<usize>) value.

Using the Question Mark (?) Operator

We will show two different ways to define the function find_char_index_in_first_word using the ? operator. Remember what was the original function definition? Let’s check it out one more time.

fn find_char_index_in_first_word(text: &str, char: &char) -> Option<usize> {
    let first_word =  match text.split(" ").next().is_some() == true {
        true => text.split(" ").next().unwrap(),
        false => return None
    };

    first_word.find(|x| &x == char)
}

We use match, before splitting the text string literal and using the next() method to advance the iterator, with the purpose of determining whether the iteration has finished or not by checking if the value is Some(item).

On one hand, if the iterator is Some(item), we unwrap the value of Some(item), which is used later in the code with the find method to find the index of the character char.

On the other hand, if the iterator is None, the function won’t execute subsequent lines of code as it will return None to the caller function.

We can achieve the same using the ? operator. The ? operator returns None whenever there is the value is not Some<usize>.

fn find_char_index_in_first_word(text: &str, char: &char) -> Option<usize> {
    let first_word = text.split(" ").next()?;
    let index = first_word.find(|x| &x == char)?;

   Some(index)
}

There are a couple of things to look for from checking the code that gets the value of first_word or text.split(" ").next()?

  1. There is no need to unwrap the Option value returned after triggering next()
  2. There is no need to use the return keyword to return None

The ? operator magically extracts the value of the next iterator if the value is Some(item).

In the case the next iterator is None, it will behave as return None, which prevents executing any additional logic written in the function and immediately return None to the caller function.

If we look further down the code in the function, the index variable works in a similar way.

let index = first_word.find(|x| &x == char)?;

If the method find() returns Some(element), it will extract the value from Some(index) and assign the value element to the index variable. If it doesn’t find anything that meets the condition defined in the closure, it returns None to the caller function.

Look at another variation of find_char_index_in_first_word method.

fn find_char_index_in_first_word(text: &str, char: &char) -> Option<usize> {
    Some(text.split(" ").next()?.find(|x| &x == char)?)
}

Notice we are executing the same logic from the original find_char_index_in_first_word.

// long way without using question mark operator ?
fn find_char_index_in_first_word(text: &str, char: &char) -> Option<usize> {
    let first_word =  match text.split(" ").next().is_some() == true {
        true => text.split(" ").next().unwrap(),
        false => return None
    };

    first_word.find(|x| &x == char)
}

// understanding the ? question mark error propagation
fn find_char_index_in_first_word(text: &str, char: &char) -> Option<usize> {
    let first_word = text.split(" ").next()?;
    first_word.find(|x| &x == char)
}

// Shortcut
fn find_char_index_in_first_word(text: &str, char: &char) -> Option<usize> {
    Some(text.split(" ").next()?.find(|x| &x == char)?)
}

Notice how the ? operator is often referred to as a shortcut for propagating errors. We went from having 4 lines of code to 2 lines of code. We even converted it into one line of code in our latest version of the find_char_index_in_first_word function because the ? allows us to chain the logic, even if we come across at any point with the values None or Err, as ? operator will take care of it by returning the error right away.

When and Where to Use the Question Mark (?) Operator?

Unfortunately, there are scenarios where you can or cannot use the question mark ? operator. As mentioned in the definition, the ? operator is used only in functions that return Result or Option types.

fn my_fn_one() -> Result<i32, ParseIntError> {}
fn my_fn_two() -> Option<usize> {}

This doesn’t mean it is not possible to work with Result and Option types inside a function. However, the ? operator should exclusively be used with types that return the same type in a function.

In other words, if the function returns Result, use the ? operator in a Result type. If the function returns an Option, use the ? operator in an Option type.

Attempting to use the ? operator in both types, Result and Option, in a function that only returns one of the two types will lead to errors. For example, if we were to write the following code:

fn bad_fn() -> Result<i32, String> {
    let b = Ok("Got it!")?;
    let a = Some(1)?;

    Ok(a)
}

Trying to run it will cause the following error:

the ? operator can only be used on Results, not Options, in a function that returns Result

Remember, the ? is a shortcut for error propagation.

If the ? is used in a type different from the type a function returns, there could be a chance of propagating errors unrelated to the type defined on a function to return.

Luckily, Rust is smart enough to detect these errors during compilation time. Hence, errors like this would not occur when running the code.

Conclusion

In conclusion, talking about the ? operator is talking about error propagation but also writing less code. Why? because the ? operator is capable of both:

  • Extracting the value of types such as Result and Option, allowing developers to not worry extracting or “unwrapping” values.
  • Returning the error type defined in the return type of a function without the need to explicitly use the return type and return an error based on the return type of a function.

Was this article helpful?

I hope this article helped you to clarify doubts and concepts of Rust, especially to those new to the programming language.

Share your thoughts by replying on Twitter of Become A Better Programmer or to personal my Twitter account.

Today, we are excited to say

println!(«New post announcement!») in #rustlang

In this new Rust article we are explaining the meaning of the question mark (?) operator in #Rusthttps://t.co/ggbEEOZGB7

— Become A Better Programmer (@bbprogrammer) March 9, 2022

Join the DZone community and get the full member experience.

Join For Free

Error Propagation means the code that detects the problem must propagate error information back to the caller function so that it can handle the problem. In the real world, the practice of Error Propagation is necessary. One of the benefits is that your code will look cleaner by simply propagating error information back to the caller that can handle the error. Another benefit is that your function doesn’t need extra code to propagate both the successful and unsuccessful cases back to the caller. I won’t go into details of this concept.

In this article, I will explain how you can do this in Rust in different ways.

  1. Using match 
use std::fs::File;
use std::io::Error;

fn open_file() -> Result<(), Error> {
    let file: Result<File, Error> = File::open("hello.txt");
    match file {
        Ok(_) => Ok(()),
        Err(e) => Err(e),
    }
}

fn main() {
    match open_file() {
        Ok(_) => println!("File is opened successfully!"),
        Err(e) => panic!(
            "Not able to open file. Here is the reason {:?}",
            e.to_string()
        ),
    }
}

If you are Rust developer, you would have known Result type, which is used to handle recoverable errors. In the above example, we are trying to open the file. File::open returns Result<File, Error>. We handle this value using match. If the file opened successfully, open_file will return Ok otherwise, pass the error value back to the caller function. Now caller function has to decide what to do with this error value. It can either create a new file hello.txt or show an error message.

2) Using try! 

The above example can be written in a much shorter way. Let’s try with try. try! is a macro that returns Errs automatically in case of error otherwise it returns Ok variant. To use this macro, you have to use the raw-identifier syntax: r#try.

fn open_file() -> Result<(), Error> {
    let file = r#try!(File::open("hello.txt"));
    Ok(())
}

try! unwraps a result and early returns the function, if an error occurred.

3) Using ?operator 

We can even shorten above code using ? operator. This operator was added to replace try! and it’s more idiomatic to use ? instead of try!.

fn open_file() -> Result<(), Error> {
    let file = File::open("hello.txt")?;
    Ok(())
}

As you can see this eliminates a lot of boilerplate and makes implementation simpler. I hope you enjoy reading this article. Let me know your thoughts in the comments!

Rust (programming language)

Cover image for How to Handle Errors in Rust: A Comprehensive Guide

Nathan

Nathan

Posted on Dec 2, 2022

• Updated on Dec 15, 2022

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).

Image description

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.

Image description

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.

Image description

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 :)

Image description

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)
}

Enter fullscreen mode

Exit fullscreen mode

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:

Image description

Or the same message error can be displayed multiples times.

Image description

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

Image description

Then our function will look like:

Image description

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.

Image description

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:

Image description

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:
Image description

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

Image description

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

Our code looks like this now:

Image description

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.

Image description

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.

Image description

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.

Image description

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.

I started doing university lectures on Rust, as well as holding workshops and trainings. One of the parts that evolved from a couple of slides into a full-blown session was everything around error handling in Rust, since it’s so incredibly good!

Not only does it help making impossible states impossible, but there’s also so much detail to it that handling errors – much like everything in Rust – becomes very ergonomic and easy to read and use.

Making impossible states impossible #

In Rust, there are no things like undefined or null, nor do you have exceptions like you know it from programming languages like Java or C#. Instead, you use built-in enums to model state:

  • Option<T> for bindings that might possibly have no value (e.g. Some(x) or None)
  • Result<T, E> for results from operations that might error (e.g. Ok(val) vs Err(error))

The difference between the two is very nuanced and depends a lot on the semantics of your code. The way both enums work is very similar though. The most important thing, in my opinion, is that both types request from you to deal with them. Either by explicitly handling all states, or by explicitly ignoring them.

In this article, I want to focus on Result<T, E> as this one actually contains errors.

Result<T, E> is an enum with two variants:

enum Result<T, E> {
Ok(T),
Err(E),
}

T, E are generics. T can be any value, E can be any error. The two variants Ok and Err are globally available.

Use Result<T, E> when you have things that might go wrong. An operation that is expected to succeed, but there might be cases where it doesn’t. Once you have a Result value, you can do the following:

  • Deal with the states!
  • Ignore it
  • Panic!
  • Use fallbacks
  • Propagate errors

Let’s see what I mean in detail.

Deal with the error state #

Let’s write a little piece where we want to read a string from a file. It requires us to

  1. Read a file
  2. Read a string from this file

Both operations might cause a std::io::Error because something unforeseen can happen (the file doesn’t exist, or it can’t be read from, etc.). So the function we’re writing can return either a String or an io::Error.

use std::io;
use std::fs::File;

fn read_username_from_file(path: &str) -> Result<String, io::Error> {
let f = File::open(path);

/* 1 */
let mut f = match f {
Ok(file) => file,
Err(e) => return Err(e),
};

let mut s = String::new();

/* 2 */
match f.read_to_string(&mut s) {
Ok(_) => Ok(s),
Err(err) => Err(err),
}
}

This is what happens:

  1. When we open a file from path, it either can return a filehandle to work with Ok(file), or it causes an error Err(e). With match f we’re forced to deal with the two possible states. Either we assign the filehandle to f (notice the shadowing of f), or we return from the function by returning the error. The return statement here is important as we want to exit the function.
  2. We then want to read the contents into s, the string we just created. It again can either succeed or throw an error. The function f.read_to_string returns the length of bytes read, so we can safely ignore the value and return an Ok(s) with the string read. In the other case, we just return the same error. Note that I didn’t write a semi-colon at the end of the match expression. Since it’s an expression, this is what we return from the function at this point.

This might look very verbose (it is…), but you see two very important aspects of error handling:

  1. In both cases you’re expected to deal with the two possible states. You can’t continue if don’t do something
  2. Features like shadowing (binding a value to an existing name) and expressions make even verbose code easy to read and use

The operation we just did is often called unwrapping. Because you unwrap the value that is wrapped inside the enum.

Speaking of unwrapping

Ignore the errors #

If you’re very confident that your program won’t fail, you can simply .unwrap() your values using the built-in functions:

fn read_username_from_file(path: &str) -> Result<String, io::Error> {
let mut f = File::open(path).unwrap(); /* 1 */
let mut s = String::new();
f.read_to_string(&mut s).unwrap(); /* 1 */
Ok(s) /* 2 */
}

Here’s what happens:

  1. In all cases that might cause an error, we’re calling unwrap() to get to the value
  2. We wrap the result in an Ok variant which we return. We could just return s and drop the Result<T, E> in our function signature. We keep it because we use it in the other examples again.

The unwrap() function itself is very much like what we did in the first step where we dealt with all states:

// result.rs

impl<T, E: fmt::Debug> Result<T, E> {
// ...

pub fn unwrap(&self) -> T {
match self {
Ok(t) => t,
Err(e) => unwrap_failed("called `Result::unwrap()` on an `Err` value", &e),
}
}

// ...
}

unwrap_failed is a shortcut to the panic! macro. This means if you use .unwrap() and you don’t have a successful result, your software crashes. 😱

You might ask yourself: How is this different from errors that just crash the software in other programming languages? The answer is easy: You have to be explicit about it. Rust requires you to do something, even if it’s explicitly allowing to panic.

There are lots of different .unwrap_ functions you can use for various situations. We look at one or two of them further on.

Panic! #

Speaking of panics, you can also panic with your own panic message:

fn read_username_from_file(path: &str) -> Result<String, io::Error> {
let mut f = File::open(path).expect("Error opening file");
let mut s = String::new();
f.read_to_string(&mut s).unwrap("Error reading file to string");
Ok(s)
}

What .expect(...) does is very similar to unwrap()

impl<T, E: fmt::Debug> Result<T, E> {
// ...
pub fn expect(self, msg: &str) -> T {
match self {
Ok(t) => t,
Err(e) => unwrap_failed(msg, &e),
}
}
}

But, you have your panic messages in your hand, which you might like!

But even if we are explicit at all times, we may want our software not to panic and crash whenever we encounter an error state. We might want to do something useful, like providing fallbacks or … well … actually handling errors.

Fallback values #

Rust has the possibility to use default values on their Result (and Option) enums.

fn read_username_from_file(path: &str) -> Result<String, io::Error> {
let mut f = File::open(path).expect("Error opening file");
let mut s = String::new();
f.read_to_string(&mut s).unwrap_or("admin"); /* 1 */
Ok(s)
}
  1. "admin" might not be the best fallback for a username, but you get the idea. Instead of crashing, we return a default value in the case of an error result. The method .unwrap_or_else takes a closure for more complex default values.

That’s better! Still, what we’ve learned so far is a trade-off between being very verbose, or allowing for explicit crashes, or maybe having fallback values. But can we have both? Concise code and error safety? We can!

Propagate the error #

One of the features I love most with Rust’s Result types is the possibility to propagate an error. Both functions that might cause an error have the same error type: io::Error. We can use the question mark operator after each operation to write code for the happy path (only success results), and return error results if something goes wrong:

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

In this piece, f is a file handler, f.read_to_string saves to s. If anything goes wrong, we return from the function with Err(io::Error). Concise code, but we deal with the error one level above:

fn main() {
match read_username_from_file("user.txt") {
Ok(username) => println!("Welcome {}", username),
Err(err) => eprintln!("Whoopsie! {}", err)
};
}

The great thing about it?

  1. We are still explicit, we have to do something! You can still find all the spots where errors can happen!
  2. We can write concise code as if errors wouldn’t exist. Errors still have to be dealt with! Either from us or from the users of our function.

The question mark operator also works on Option<T>, this also allows for some really nice and elegant code!

Propagating different errors #

The problem is though, that methods like this only work when the error types are the same. If we have two different types of errors, we have to get creative. Look at this slightly modified function, where we open and read files, but then parse the read content into a u64

fn read_number_from_file(filename: &str) -> Result<u64, ???> {
let mut file = File::open(filename)?; /* 1 */

let mut buffer = String::new();
file.read_to_string(&mut buffer)?; /* 1 */

let parsed: u64 = buffer.trim().parse()?; /* 2 */

Ok(parsed)
}

  1. These two spots can cause io::Error, as we know from the previous examples
  2. This operation however can cause a ParseIntError

The problem is, we don’t know which error we get at compile time. This is entirely up to our code running. We could handle each error through match expressions and return our own error type. Which is valid, but makes our code verbose again. Or we prepare for “things that happen at runtime”!

Check out our slightly changed function

use std::error;

fn read_number_from_file(filename: &str) -> Result<u64, Box<dyn error::Error>> {
let mut file = File::open(filename)?; /* 1 */

let mut buffer = String::new();
file.read_to_string(&mut buffer)?; /* 1 */

let parsed: u64 = buffer.trim().parse()?; /* 2 */

Ok(parsed)
}

This is what happens:

  • Instead of returning an error implementation, we tell Rust that something that implements the Error error trait is coming along.
  • Since we don’t know what this can be at compile-time, we have to make it a trait object: dyn std::error::Error.
  • And since we don’t know how big this will be, we wrap it in a Box. A smart pointer that points to data that will be eventually on the heap

A Box<dyn Trait> enables dynamic dispatch in Rust: The possibility to dynamically call a function that is not known at compile time. For that, Rust introduces a vtable that keeps pointers to the actual implementations. At runtime, we use these pointers to invoke the appropriate function implementations.

Memory layout of Box and Box

And now, our code is concise again, and our users have to deal with the eventual error.

The first question I get when I show this to folks in my courses is: But can we eventually check which type of error has happened? We can! The downcast_ref() method allows us to get back to the original type.

fn main() {
match read_number_from_file("number.txt") {
Ok(v) => println!("Your number is {}", v),
Err(err) => {
if let Some(io_err) = err.downcast_ref::<std::io::Error>() {
eprintln!("Error during IO! {}", io_err)
} else if let Some(pars_err) = err.downcast_ref::<ParseIntError>() {
eprintln!("Error during parsing {}", pars_err)
}
}
};
}

Groovy!

Custom errors #

It’s getting even better and more flexible if you want to create custom errors for your operations. To use custom errors, your error structs have to implement the std::error::Error trait. This can be a classic struct, a tuple struct or even a unit struct.

You don’t have to implement any functions of std::error::Error, but you need to implement both the Debug and the Display trait. The reasoning is that errors want to be printed somewhere. Here’s how an example looks like:

#[derive(Debug)] /* 1 */
pub struct ParseArgumentsError(String); /* 2 */

impl std::error::Error for ParseArgumentsError {} /* 3 */

/* 4 */
impl Display for ParseArgumentsError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}

  1. We derive the Debug trait.
  2. Our ParseArgumentsError is a tuple struct with one element: A custom message
  3. We implement std::error::Error for ParseArgumentsError. No need to implement anything else
  4. We implement Display, where we print out the single element of our tuple.

And that’s it!

Anyhow… #

Since a lot of the things you just learned a very common, there are of course crates available that abstract most of it. The fantastic anyhow crate is one of them and gives you trait object-based error handling with convenience macros and types.

Bottom line #

This is a very quick primer on error handling in Rust. There is of course more to it, but it should get you started! This is also my first technical Rust article, and I hope many more are coming. Let me know if you liked it and if you find any … haha … errors (ba-dum-ts 🥁), I’m just a tweet away.

  1. the Question Mark Operator (?) in Rust
  2. Error Propagation in Rust
  3. When and Where to Use the Question Mark (?) Operator in Rust

Question Mark Operator in Rust

This tutorial is about Rust’s question mark operator (?).

the Question Mark Operator (?) in Rust

The question mark operator (?) unwraps fair values and returns erroneous values to the calling function, which is then passed on to the caller. A unary postfix operator can only be applied to the types Result<T, E> and Option<T> and cannot be used to any other types.

Things can become cluttered when using the match operator to chain results together; fortunately, the ? operator can make things a little more orderly. ? is used at the end of an expression that returns a Result.

It is equivalent to a match expression in which the Err(err) branch expands to an early return Err(From::from(err)), and the Ok(ok) branch expands to an ok expression, and the Err(err) branch expands to an early return Err(From::from(err)) branch.

? cannot be overloaded.

As you may have seen, Rust is a rule-following language. Therefore, although it contains panics, their use for error handling is discouraged as they are meant for unrecoverable errors.

Additionally, we must study error propagation to understand how to use the ? mark directly.

Error Propagation in Rust

Error propagation is the process of propagating or spreading up, or returning error information found in code that is often invoked by a caller function to enable the caller function to handle the problem effectively.

Example:

fn main() {
    let result = char_in_second_word(&"Learning when question mark used", &'i');

    println!("The value is {}", result.unwrap_or(1))
}


fn char_in_second_word(sentence: &str, char: &char) -> Option<usize> {
    let second_word =  match sentence.split(" ").next().is_some() == true {
        true => sentence.split(" ").next().unwrap(),
        false => return None
    };

    second_word.find(|x| &x == char)
}

Output:

Take note of the simple utility function char_in_second_word, which returns the index of the character discovered in the first word detected on a literal string.

However, if the string literal is Learning and the character we’re looking for is I, the returned index value will be Some(5) because the string literal’s first word is Learning, which contains the letter I in position 5 of the array of characters.

But using a question mark, the codes can be made easier and simpler.

In Rust, Result is used for error handling. The ? operator can be used only in a function that returns Result or Option.

Example:

use std::num::ParseIntError;

fn main() -> Result<(), ParseIntError>
{
    let num = "7".parse::<i32>()?;
    println!("{:?}", num);
    Ok(())
}

Output:

The ? operator could be used on a Result type if the function contained also returns a Result.

Each time you see a ?, it indicates the possibility of an early return from the function in which the ? is utilized.

When and Where to Use the Question Mark (?) Operator in Rust

There are some circumstances where utilization of the question mark ? operator cannot be made. For example, the description states that the ? operator is only used in functions that return the types Result or Option.

This does not exclude working with the Result and Option types within a function. The ? operator, on the other hand, should be used strictly with types that return the same type in a function.

In other words, if the function returns a type named Result, utilize the ? operator in a type named Result. If the function returns an Option, the ? operator should be used in an Option type.

Attempting to use the ? operator on both Result and Option types in a function that returns only one of the two kinds will result in an error. For illustration, consider the following code.

fn err_fxn() -> Result<i32, String> {
    let x = Ok("The go!")?;
    let y = Some(1)?;

    Ok(x)
}

Running the above source code will cause the error as stated below.

the `?` operator can only be used on `Result`s, not `Option`s, in a function that returns `Result`

We should remember that, in error propagation, the ? is used as a shortcut.

If the ? character is used in a type other than the one returned by a function, there is a possibility of propagating mistakes unrelated to the type defined on the function to return.

Fortunately, Rust is intelligent enough to catch these problems during the compilation process. As a result, such mistakes would not arise during code execution.

Понравилась статья? Поделить с друзьями:
  • Rust error log
  • Rust error handling
  • Rust error code 10011
  • Rust error anyhow
  • Rust error allocating memory