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.

Results 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 on Result 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 makes Result 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 using expect, 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 expects or unwraps.

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 unwraps and expects, 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 Errs 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 Errs 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 Results, 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.