What is Rust's Question Mark Operator For?
This blog post is meant for beginners to the Rust programming language, but not necessarily
beginners to programming - it assumes at least a basic familiarity with some other popular
languages. It will introduce the basics of error handling in Rust with relation to the ?
operator.
When I encountered Rust code for the first time, I noticed that it was littered with ?s all over
the place. Because I was working with a lot of JavaScript at the time, my mind first jumped to “it
must be something like JS’s optional chaining feature, right? Ehhh… not exactly.
To really understand the ? here, we first need to understand one of the most fundamental parts of
Rust, or really any programming language: error handling.
You’re likely familiar with the concept of the Exception. Exceptions are used for error handling in many of the most popular languages, from Java and C#, to JavaScript, Python, C++ and more. The way Rust deals with errors is different. Different, however, in a way I find very appealing.
While a JavaScript function that can fail may return a value or throw an exception (which may or
may not be “caught” by the caller) a similar Rust function returns a value of type Result.
The Result Type
Result is a Rust enum. Enums in Rust are very powerful and flexible, much more so than in
many other languages. We’re not going to dive too deep into that right now, but you can read more
about them in the Rust book. The most
important thing to know for now is that Rust’s enums can hold values.
The variants of Result are Ok, indicating a successful response, and Err, indicating an
unsuccessful one. The Ok variant holds the “actual” value returned by the function. Result is
actually a generic type, Result<T, E>, where T is that effective return type of the
function, and E is the type of the error that might be encountered from an unsuccessful call.
Let’s look at a small concrete example. First, a trivial JavaScript function:
function divide(a, b) {
if (b === 0) {
throw new Error('Hey, that\'s illegal!');
}
return a / b;
}
Not much going on here, but it shows a small example of throwing an Exception (Error) in JavaScript. Let’s look at an equivalent function in Rust:
fn divide(a: f32, b: f32) -> Result<f32, String> {
if b == 0.0 {
return Err("Hey, that's illegal!".to_string());
}
return Ok(a / b);
}
As you can see, the Rust function returns a Result<f32, String>. f32 is effectively the “actual”
return type of the function, and in this case we’re simply using a String (*see note below)
as the error type to convey an error message. No exceptions thrown anywhere.
*Note: The type of an error is often more structured and contains more information than just a raw string. But for this small example, String is simple and easy.
The beautiful thing about this is that Rust can force you to consider any and all errors that might be encountered during the course of your program at compile time. Think about that for a second. That means that once your code compiles, you can be sure that there are no errors that you haven’t handled and which might crash your program!
So where does the question mark come in?
Now that we have Result out of the way, we can get to the question mark. To see an example of this,
let’s look at how the previous function might be called.
fn main() -> Result<(), String> {
let result: Result<f32, String> = divide(1.0, 2.0);
let answer = match result {
Ok(answer) => answer,
Err(error) => {
return Err(error);
},
};
println!("Done!: {}", answer);
Ok(())
}
A match statement is another fundamental Rust concept that you can read about here, but for the purposes of this example you can
think of it a bit like a switch statement. Here, we are setting answer equal to the “result”
of the function call if it is Ok, otherwise we are returning the Err error back out of this
calling-function (main).
It turns out that this scenario is an incredibly common pattern in Rust, so there’s a bit of “sytnatic sugar” to handle it.
The above can be simplified using ?:
fn main() -> Result<(), String> {
let answer: f32 = divide(1.0, 2.0)?;
println!("Done!: {}", answer);
Ok(())
}
The ? operator can be used to “unwrap”
a Result into its held-value if it is the Ok variant, or early return the error if its the Err variant.
It is effectively just a short-hand for the logic in the previous block. Having this syntax shortcut to early return
an error is incredibly useful – something that Go programmers must dream of.
Useful Links
- The Rust Book: https://doc.rust-lang.org/book/title-page.html
- Enums: https://doc.rust-lang.org/book/title-page.html
- The Question Mark Operator: https://doc.rust-lang.org/reference/expressions/operator-expr.html#the-question-mark-operator
- Result Type Reference: https://doc.rust-lang.org/std/result/enum.Result.html#variant.Err
- Match Statement: https://doc.rust-lang.org/book/ch06-02-match.html