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.
- 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!
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 returnstrue
orfalse
. It applies this closure to each element of the iterator, and if any of them returntrue
, then find() returnsSome(element)
. If they all returnfalse
, it returnsNone
.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()?
- There is no need to unwrap the
Option
value returned after triggeringnext()
- There is no need to use the
return
keyword to returnNone
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 Result
s, not Option
s, 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
andOption
, 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.
- 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)
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).
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)
}
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:
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.
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)
orNone
)Result<T, E>
for results from operations that might error (e.g.Ok(val)
vsErr(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
- Read a file
- 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:
- When we open a file from
path
, it either can return a filehandle to work withOk(file)
, or it causes an errorErr(e)
. Withmatch f
we’re forced to deal with the two possible states. Either we assign the filehandle tof
(notice the shadowing off
), or we return from the function by returning the error. Thereturn
statement here is important as we want to exit the function. - We then want to read the contents into
s
, the string we just created. It again can either succeed or throw an error. The functionf.read_to_string
returns the length of bytes read, so we can safely ignore the value and return anOk(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 thematch
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:
- In both cases you’re expected to deal with the two possible states. You can’t continue if don’t do something
- 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:
- In all cases that might cause an error, we’re calling
unwrap()
to get to the value - We wrap the result in an
Ok
variant which we return. We could just returns
and drop theResult<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.rsimpl<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)
}
"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?
- We are still explicit, we have to do something! You can still find all the spots where errors can happen!
- 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)
}
- These two spots can cause
io::Error
, as we know from the previous examples - 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.
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)
}
}
- We derive the
Debug
trait. - Our
ParseArgumentsError
is a tuple struct with one element: A custom message - We implement
std::error::Error
forParseArgumentsError
. No need to implement anything else - 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.
- the Question Mark Operator (
?
) in Rust - Error Propagation in Rust
- When and Where to Use the 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.