Why Rust should only have provided expect
for turning errors into panics, and not also provided unwrap
UPDATE 2: I have made the title longer because people seem to be
insisting on misunderstanding me, giving examples where the only
reasonable thing to do is to escalate an Err
into a panic. Indeed,
such situations exist. I am not advocating for panic-free code.
I am advocating that expect
should be used for those functions,
and if a function is particularly prone to being called like that
(e.g. Mutex::lock
or regex compilation), there should be a panicking
version.
UPDATE: This post
by Andrew Gallant, author of the excellent
ripgrep
, is a good
overall discussion of the topic I am trying to address here. I
basically entirely agree with it and recommend it as very
educational; specifically, I disagree only in that I think
that linting for unwrap
is a good thing, for the reasons
he acknowledges but ultimately does not find compelling in that
section. In his own terms, I just think that the juice is worth the squeeze.
I see the unwrap
function called a lot, especially in example code,
quick-and-dirty prototype code, and code written by beginner Rustaceans.
Most of the time I see it, ?
would be better and could be used instead
with minimal hassle, and the remainder of the time, I would have used
expect
instead. In fact, I personally never use unwrap
, and I even
wish it hadn’t been included in the standard library.
The simple reason is that something like expect
is necessary and
sometimes the best tool for the job, but it’s necessary rarely and should
be used in the strictest moderation, just like panicking should be used
in strictest moderation, and only where it is appropriate (e.g. array
indexing, for reasons I elaborate on later). unwrap
is too easy and
indiscriminate, and using it at all encourages immoderate use.
This has turned out, much to my surprise, to be a somewhat controversial stance, and so I’d like to take some time to explain why I feel that way.
I’ll begin by reviewing what Result
is and what options we have
for dealing with its recoverable errors.
Result
s and what to do with them#
Rust is widely and rightly praised for its use of
Result
for
recoverable error handling. Instead of using exceptions like C++, which
propagate invisibly and surprisingly, or using sentinal values like NULL
and -1
, Rust has sum types
and thus, a function can return a value that is either an error (of a
specified, potentially narrow type) or the value we want:
#[derive(Copy, PartialEq, PartialOrd, Eq, Ord, Debug, Hash)]
#[must_use = "this `Result` may be an `Err` variant, which should be handled"]
pub enum Result<T, E> {
Ok(T),
Err(E),
}
If we have a function call foo()
that can fail and therefore returns
a Result
, we have a few different tactics we can use to handle it:
- Ignore: We can ignore the return value, and therefore also ignore
whether it errors. This is almost never what we actually want, and so the
#[must_use]
annotation onResult
causes a warning to be issued:
foo(); // WARNING
- Manual We can manually
match
on the return value and do different things:
match foo() {
Ok(value) => do_something(value),
Err(err) => handle_error(err),
}
- Propagate: We can propagate the error ergonomically using the
?
operator. This makesResult
work like exceptions in many of the good ways, while cluing in the reader to the additional control flow, which is good:
foo()?;
- Panic with custom message: We
can transform the error into an “unrecoverable”
panic
usingexpect
, which takes a string argument which is used to customize the error message:
foo().expect("foo error");
- Panic without custom message: We can transform the error into a
panic with
unwrap
, which does not take a string argument and therefore leads to more generic error messages:
foo().unwrap();
Most of the time, in production code, we will want to go with the propagate option, especially in library code where the application will likely have a better notion of what to do with the error. This option makes the flow control clearer, tends to result in better error messages when the error messages are ultimately outputted, and gives the calling functions more options.
The manual option is useful even in a library for when an error is in fact recoverable at that particular point (e.g. by retrying). In an application, we’re often at a point where it makes sense to report the error (via a log message or console output or user-facing error message).
Sometimes, errors are in fact no big deal, and should be suppressed completely, but this is better expressed through the manual option at the application layer, with a comment explaining why the right thing is to do nothing.
But sometimes (and only sometimes), panic is appropriate, and
for that, there are two options, unwrap
and expect
. I always prefer
expect
for this, and pretend that unwrap
doesn’t exist, because it
makes panicking too easy. To explain, I’d like to discuss in what
situations I think panics are appropriate.
When to panic?#
Escalating an Err
result to a panic should be done
in similar situations to when panic
is appropriate
in general, which the Rust book offers some guidance
on.
The most clear-cut case is when a code path is a logic error, when the error is only possible if the programmer has made a mistake and an invariant has been broken.
A typical example is array indexing. We often find ourselves with
an array index that we (think we) know is valid and we want to use it to
index an array, because we got it from looping or otherwise operating on
the array bounds. We’re not so confident that we want to use unsafe
and do an unchecked array access – that could result in a security
vulnerability if we’re wrong – but it would also be nonsensical to try
to recover from such an invalid access.
For array indexing, this is actually the most
common scenario, and so the index operator in Rust
actually panics for us if we specify an out-of-bounds
index. An unsafe
checked array indexing method container is
available,
as is one that never panics and instead returns an
Option
,
but most of the time we want the panicking checked operation
and so that is the version that gets the syntactic
sugar:
arr[index]
will neither memory corrupt nor return a recoverable
error on an invalid index, but instead panic.
This is definitely the best default for array indexing. But sometimes,
logic errors result in recoverable errors, in Err
(or its equivalent
in the Option
world, None
). For example, maybe you have an array-like
data structure in which only a get
method is available, which
returns an Option
. If you were confident in your indexing, you
would want to panic on None
, and you can call expect
or unwrap
to make that happen.
It is certainly more ergonomic to write expect
than to do the
match
manually, and less likely to lead to mistakes:
let val = arr.get(i).expect("i should be valid index");
let val = match arr.get(i) {
Some(val) => val,
None => panic!("i should be valid index"),
}
Besides logic errors, panics are also relevant in test cases, where they are used to indicate test failure.
No need to panic: Propagation Made Easy#
However, expect
and unwrap
– especially unwrap
– are also
amenable to overuse and misuse.
Perhaps you’re doing prototyping and just need something that works most
of the time, or you’re writing a simple app with limited error-handling
needs. Some people use unwrap
and expect
for this situation, but
I don’t. I use ?
even in that situation, because I never know when
prototype code might have to escalate to production code – either
so suddenly there’s no time for me to intervene and improve the
error handling or so gradually there’s no occasion for it and it
never gets prioritized. Fixing crappy usage of ?
in such a situation
is way easier and more likely to happen than fixing a bunch of
expect
s or unwrap
s.
How can I prototype with ?
? Doesn’t it require a lot of extra work,
compared with unwrap
? Honestly, not really. Writing Result<Foo>
is not substantially harder than writing Foo
for functions which
can error. As for converting between error types, libraries like
eyre
and
anyhow
exist so that
all errors can be included.
Example code similarly can be written with ?
. This is important because
Rust is rapidly growing and has a lot of new programmers using it. They
see that a function returns a thing, and want to get to the thing and
don’t know how to, and they see unwrap
in the example code and they
cargo cult it. Even if they have learned a thing or two about Rust,
it does have the perfect type signature for their problem, and so they
jump on it, and end up using it in prototype code and then trying to use
it in production code. Perhaps they know about ?
, but it has a higher
barrier to entry, and so they’ll procrastinate learning about it.
In these situations, unwrap
provides an easy, ergonomic
way of calling a function that might error, and so it’s very tempting,
like walking through the grass when there’s a paved path
available. However,
?
is generally preferable to unwrap
or expect
, and so the
relative easiness is misaligned to the order of preferences.
And unfortunately, once code has been written using unwrap
or expect
heavily, it’s hard to adapt it to use ?
and propagation,
especially if those interfaces have come to be relied upon.
Why I prefer expect
to unwrap
#
There are definitely legitimate use cases to turning an error
into panic, but they are relatively rare, especially if the
code is well-factored. Turning an error into a panic is also
extremely tempting to be abused. The second situation is more
common than the first, so in many codebases, the bad unwrap
s
and expect
s, the sloppy “OK for now” ones or the “it’s just
an example” ones outnumber the legitimate use cases.
Raising the barrier to entry seems like a good solution, and expect
seems like the perfect balance. The error string can also serve
as documentation of why this decision was made, like comments for
unsafety. The fact that expect
is a little less ergonomic is a
feature, as it discourages casual use. expect
has enough convenience
to encapsulate the concept of escalation from a “recoverable” error
to an “unrecoverable” error, but not so much that it competes with ?
in ergonomics.
expect
’s error message can serve as a comment as to why the
panic is justified. Comments are a good thing, and for as
questionable an operation as escalating an Err
to a panic,
it’s useful to explain why we think it will never happen even if
we think it’s obvious. Like the comments recommended for unsafe
blocks, I think that expect
is a situation that deserves some
indication to the reader as to why the author thinks this is OK.
Why have this in the error message rather than just a comment?
expect
’s error message is also helpful in debugging. unwrap
can give
good error messages, printing the error value and providing a backtrace,
but in other configurations and deployments you might not see a backtrace
and the error value might not be useful. Some unwrap
calls might
provide good enough error messages sometimes, but it doesn’t work 100%
of the time, so it can’t be relied upon – especially when expect
is
readily available. Especially in the case of a logic error,
when the condition was thought impossible, debugging will already
be hard, and the person doing the debugging needs all the help they
can get.
Objections#
When I’ve expressed my opinions about unwrap
before, one objection
stands out in my mind as particularly interesting and particularly
valid. I say above that legitimate use cases to turning an Err
into
a panic are rare, which is generally true, but sometimes
can seem false. There are certain APIs where it comes up a lot,
APIs where Err
s frequently are actually logic errors.
For example, regular expressions. The
regex
crate uses a method called
new
that is used to prepare regular expressions. It is practically always
called on a constant string, making any failure a logic error, which
should result in a panic, as discussed above. However, this same new
method returns a Result
, necessitating an unwrap
or an expect
to make the logic error into a panic. Am I seriously suggesting that
the poor user write .expect("bad regular expression")
instead of
.unwrap()
every time?
Well, that puts regex compilation in the same category as array
indexing in my mind, and means that the default regex compilation function
should panic on the user’s behalf (of course, the Result
version
should still be possible, just as get
is a possible function for
slices).
Similarly, when I’ve expressed my opinions about unwrap
, some have
assumed I’m opposed to panics altogether, and asked me if I used array
indexing, implying that if I accept the possibility of panics in array
indexing, I should accept the possibility of panics in unwrap
as well.
For both of these objections, I want to clarify something: I’m not
opposed to panicking in logic error situations. But that does not imply
that unwrap
is a good idea. Most Err
s are not logic errors, and so
converting one to a panic should be a little inconvenient, and should
require the user to think enough to write an error message.
For those situations where an error is actually likely to be a logic
error, such as array indexing or regex compilation, returning Result
need not be the function’s default behavior. Perhaps the author of
regex
can make new
panic on compiler error, and another
function can be written for when the regex in question was user
inputted, or where a regex compilation error would not be a logic error.
In general, when you find yourself using expect
or unwrap
over and
over again in the same way, and you’re sure it’s legitimate each time,
do what you do with all smelly-seeming code if you know it’s actually
the right thing in spite of the smell: Wrap it in an abstraction. Put
it in a function that calls expect
to panic on error.
This is not cheating. This new, panicking function would instead serve
as a documentation for the fact that in this context, an Err
is in
fact likely to be a logic error, a tangible paper trail that someone
made a conscious call that, as a policy, panicking is appropriate in
this instance. The decision to panic instead of returning an Err
in
this situation is made in one place instead of many, where it can be
explained in a detailed comment if desired, and where it certainly won’t
be too much of a burden to use expect
instead of unwrap
. Even the
fact of the function existing and having a panic-based interface is a
signal from the library author that they have thought about this issue,
and deemed the situation to be more analogous to array indexing than,
say, a file-not-found.
Tendencies and Statistics#
In any case, array indexing and regex compilation are the exceptions, not the rule. Almost all bounds checks failures may be logic errors. Almost all regex compilation errors may be logic errors. Making these functions panic would indeed do little damage, as panicking is almost always the right move.
But – and this is a big “but” – most functions, when they return
Err
, genuinely are signalling recoverable errors, and unwrap
doesn’t discriminate – it works equally well on all of them, in
the inappropriate situations as well as the appropriate situations.
With array indexing and regex compiling, the nature of the function being
called gives some indication of why it’s a logic error; with unwrap
,
there is no indication.
Generally, this argument is in terms of statistics and human nature, not
in terms of absolutes. Turning an Err
into a panic should be rare, not
necessarily in terms of how often it happens, but in how often it shows up
in code. If it is common, either the programmer is using bad practices,
and should be using better practices, or the API has a design flaw, and
that needs to be fixed. In either case, expect
is better than unwrap
.
Ideally, we don’t get used to seeing expect
and unwrap
being used
all the time. We don’t get used to casually panicking on Err
, but instead
treat panicking like an operation that should be considered carefully,
whether once for all instances of a specific call (as in array indexing
or regex construction), or on a case-by-case basis (for other uses of
expect
).
Humans are creatures of habit and lazy by nature. unwrap
is a powerful
tool, a way to get around the type system, and as such, we might find
ourselves addicted to it. We should treat even expect
as mildly
suspicious, something only to be used with consideration, something
to be wrapped behind an abstraction (as in the regex case). unwrap
is even more dangerous, because it is easier, and given that legitimate
usage of except
should be rare (again in terms of lines of code, not
frequency of invocation) and hidden behind an abstraction when it is
common in frequency of invocation, I see no need for unwrap
to exist.
Context#
I am aware that removing unwrap
from Rust is not a viable option at
this point, which is why I said that I wish it was never put in Rust to
begin with. I am aware that unwrap
is used in the Rust compiler,
and that there is no consensus to avoid unwrap
to the level that I
avoid it.
I will however note that the documentation
of unwrap
comes with a warning not to use
it.
The warning is framed in terms of the fact
that unwrap
may panic, but the documentation of
expect
,
where this is equally true, does not come with such a warning.
Conclusion#
Escalating an Err
to a panic is sometimes appropriate. But it should
be a considered choice, either on a function-by-function basis (through
a wrapper function calling expect
or a different choice of interface), or
on a case-by-case basis. In either case, unwrap
makes it too easy.
Including an error message, and documenting why a panic is appropriate
(either through the error message or separately) should not be too much
to ask. If it is, that’s a code smell. The fact that expect
is more
difficult is a feature.
In this article I have mentioned only briefly the other motivation for
using expect
– better error messages for debugging. I thought the
code smell argument was more important. But debuggability can be very
important as well, so I’ll discuss it briefly here. I don’t think it’s
safe to assume backtraces will always be available. I don’t think it’s
safe to assume every use of unwrap
will print a useful error message,
even if it sometimes can. Maybe an individual use of unwrap
in one
context does not cause this problem, but once unwrap
is established
as acceptable, it opens the door for it to be abused.
I personally do not use unwrap
, nor do I sign off on code that does.
I even prefer expect("foo")
to unwrap
, because it signals that it’s
off-the-cuff example code and shows that the person writing it knows
that more consideration would be needed to put it into production.
Please consider joining me in this approach.
If you do not want to implement so strict a policy, and you think I’m too
extreme in this way, hopefully this article at least makes my argument
clearer, and explains why I do not call unwrap
but still feel
comfortable indexing my arrays. Hopefully also this has given food
for thought about Result
s, errors, and panics.
Edits#
This post has been edited to clarify certain things, including a clarification in the opening to the post to make sure my overall position is easily comprehensible.
Subscribe
Find out via e-mail when I make new posts! You can also use RSS (RSS for technical posts only) to subscribe!
Comments
If you want to send me something privately and anonymously, you can use my admonymous to admonish (or praise) me anonymously.
comments powered by Disqus