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.
Table of Contents
What is the question mark (?) operator 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
Rust documentationfind()
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
.
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.