I noticed that handling the None
variant of Option
without falling back on unwrap()
is a common papercut in Rust.
More specifically, the problem arises when you want to return early from a function that returns a Result
if you encounter None
.
It has been discussed a million times already, but, surprisingly, not even the Rust book mentions my favorite approach to handling that, and many forum posts are outdated.
With a bit of practice, robust handling of None
can become as easy as unwrap()
, but safer.
Jump to the end if you’re in a hurry and just need a quick recommendation.
The Problem
Very commonly, people write code like this:
// Assume that this fetches the user from somewhere
The goal here is to return early if you encounter None
in an Option
, so
they use the ?
operator to propagate errors.
Alas, this code doesn’t compile. Instead, you get a dreaded error message:
error: the `?` operator can only be used on `Result`s, not `Option`s, in a function that returns `Result`
-/lib.rs:10:26
|
9 |
Ouch. This is scary-looking!
There’s a lot of visual noise in this error message. The FromResidual
and Yeet
are implementation details which could be confusing to a new user, and the relevant details are somewhat obscured.
And all we did was try to use the ?
operator for our Option
.
My main gripe with this error message is that it doesn’t explain why the ?
operator doesn’t work with Option
in that case… just that it doesn’t.
What People End Up Doing
The most common approach I see is this:
- People are confused for a bit.
- They try to understand the error message.
- Eventually, they give up and just add
unwrap()
. - They make a mental note to come back to it later.
- ‘Later’ never comes.
Here’s what they end up with:
In trainings, I noticed that people are often too embarrassed to ask for help. They think people are supposed to “get this” and they are the only ones who don’t.
This just defers the problem.
The user of the function might hit a panic
at runtime. That user might be their future self.
unwrap
is fine in many cases, but it shouldn’t be the first intuition for dealing with unexpected situations.
Especially when you’re writing a library or a function that is part of a larger codebase, you should strive to handle
such situations gracefully.
And in production code, it sets a bad example:
one unwrap
attracts another and the codebase becomes more fragile as you continue down this path. [1]
Okay, I’ve kept you waiting long enough. Let’s demystify this error message.
The Actual Problem
What the compiler is trying to tell us is that you can’t propagate optionals within functions which return Result
.
Everything works just fine if you’re returning an Option
instead:
So if you can change the outer function to return an Option
instead, you won’t run into the above error message.
There’s more info in the Rust documentation here.
But what if the final return type of your function has to be a Result
or if you want to convey more information about the missing value to the caller?
After all, communicating the distinction between different None
values can be helpful to the user of your function.
So, what if you really want your code to look like this?
Well, that’s just a type error: get_user()
returns an Option
, but the outer function expects a Result
.
Our problem statement becomes easier:
How to return a helpful error message when an Option
is None
in this case?
Turns out, there are multiple solutions!
Solution 1: change the return type
If “not having a value” is truly an error condition in your program’s logic, you should use Result
instead of Option
. Option
is best used when the absence of a value is a normal, expected possibility, not an error state.
In our case, if you can change get_user()
to return a Result
instead of an Option
, you can use the ?
operator as you intended:
However, you might not be able to change get_user()
to return a Result
for various reasons,
for example, if it’s part of a library or if it’s used in many places
or if other callers don’t treat the absence of a user as an error.
In that case, read on!
Solution 2: ok_or
The initial error message, while cryptic, gave us a hint:
use `.ok_or?` to provide an error compatible with ` `.
Apparently we can use the ok_or
method, which converts the Option
into a Result
:
let user = get_user.ok_or?;
That’s convenient! It also chains well when we use iterator patterns:
let user = get_user
.ok_or?
.do_something
.ok_or?;
ok_or
is also great if you can recover from None
and handle that case gracefully:
let user = get_user.ok_or?;
These are good use cases for ok_or
.
On the other side, it might not be immediately obvious to everyone what ok_or
does.
I find the name ok_or
unintuitive and needed to look it up many times.
That’s because Ok
is commonly associated with the Result
type, not Option
.
There’s Option::Some
, so it could have been called some_or
, which was actually suggested in 2014, but the name ok_or
won out,
because ok_or(MyError)
reads nicely and I can see why. Guess we have to live with the minor inconsistency now.
I think ok_or
is a quick solution to the problem, but there are alternatives that might be more readable.
Solution 3: match
In the past, I used to recommend people to not be clever and just use a match
statement.
let user = match get_user ;
match
works in combination with Option
, because it’s just an enum and we can pattern match on it.
As long as we cover all cases, the compiler is happy. In this situation, we only have two cases: Some
and None
.
In the None
case, we return early with an error. In the Some
case, we continue with the value.
(match
is an expression and the value of the last expression in the block is returned. In our case it’s user
and it
gets assigned to the user
variable in the outer scope.)
This is already more explicit and easier to understand for beginners. The one issue I had when teaching this was that it looked a bit more verbose for simple cases.
Solution 4: let-else
With Rust 1.65, the let-else
expression was stabilized, so now you can write this:
let Some = get_user else ;
// Do something with user
In my opinion, that’s the best of both worlds: it’s compact while still being easy to understand. It’s unanimously loved by beginners and experienced Rustaceans alike.
For some explanation: if get_user()
returns Some
, the let
statement will destructure the Some
variant and assign the value to the user
variable. If get_user()
returns None
, the else
block will be executed and we return early with an error.
My favorite thing about let-else
is that it clearly highlights the ‘happy path’ of your code.
Unlike a match
statement where you need to read both arms to understand the intended flow, let-else
makes it immediately clear what the expected case is, with the else
block handling the exceptional case.
This is a clear winner. It is way more intuitive for beginners; once they understand the pattern, they use it all the time!
Bonus: anyhow
I wanted to add one honorable mention here.
If you’re writing an application (not a library) and you’re using the
anyhow
crate already, you can also use their context
method to handle None
:
use ;
It’s slightly less verbose than let-else
, which makes it appealing.
Just remember that anyhow
is an external dependency.
It’s probably fine for applications, but you might not want to use it in a library as users of
your library can no longer match on the concrete error variant then.
That’s why I believe that let-else
is the best solution for handling None
in most cases.
- It’s part of the standard library.
- It’s easy to understand for beginners.
- Learning the mechanics behind it is helpful in other places as well.
- It’s reasonably compact.
- It allows for more complex error handling logic in the
else
block if needed.
Conclusion
For most cases, I prefer this syntax:
let Some = some_function else ;
To me, let-else
is the best solution for handling None
because:
- It’s part of the standard library.
- It works for both libraries and applications.
- It’s easy to understand for beginners.
- It’s reasonably compact.
- It allows for more complex error handling logic in the
else
block if needed. - Learning the mechanics behind it is helpful in other places in Rust.
I hope this helps more people handle Option
in a more robust way.
If it helps a single person avoid one unwrap
, it was already worth it.
-
Sometimes,
unwrap()
can make code more readable by reducing noise, especially when the success case is overwhelmingly likely. It’s okay to useunwrap()
when you can prove that a failure is impossible or when a panic is actually the desired behavior for failures. Andrew Gallant wrote an article on this where he goes into more detail. ↩