Rust: What is the Question Mark (?) Operator?

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.

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