Using unwrap() in Rust is Okay (2022)
45 comments
·May 18, 2025Zambyte
Galanwe
From my experience, Zig is the worst at error handling.
Not that the language itself make it hard, on the contrary, but the standard library itself is littered with opinionated error handling that straight up panic with no recovery at every corner.
For instance, it was decided that the kernel returning EINVAL for any syscall is worth panicking, even though there are a lot of cases where that is recoverable. The answer from Andrew? It's the kernel's fault. Very helpful.
Quotes are cute, but their actual implementation matters more.
johnisgood
My problem with panics is that if it is a library (e.g. standard library), then I cannot handle the error the way I want. Let us say that you have a library that parses something. As the user of that library, you want to be able to handle these errors gracefully, you do not want the library to just panic at those parse errors. Sadly I have seen libraries do this.
90s_dev
> Assertions for programmer mistakes; errors for user mistakes
That's been the general mantra for at least a decade or two. And I think it's right.
littlestymaar
> which removes any temptation to use them for user errors.
It doesn't look to be too much of a temptation to use panics as regular errors in Rust though, so I don't think it gains you much.
And it makes it more complicated when you want to catch panics for legitimate reasons (like when you want to show an error box to the user, instead of silently crashing, or when you want to send the crash log for later analysis). It's still possible, by using a separate process, but it's unnecessary friction in order to prevent a sin that doesn't happen in practice.
Zambyte
> It doesn't look to be too much of a temptation to use panics as regular errors in Rust though
Maybe not, but it was enough for the author to dedicate at least one whole section of the article to reasoning about this.
> like when you want to show an error box to the user, instead of silently crashing, or when you want to send the crash log for later analysis
As I mentioned, panics can be handled but not recovered. You can use the panic handler to do these things just fine. Just not recover.
jvanderbot
I've never heard of anyone using panic for exception-style recovery. That's anathema to the whole idea of Result<T,V>. But I suppose if it's possible, it will be normal for someone.
littlestymaar
> As I mentioned, panics can be handled but not recovered. You can use the panic handler to do these things just fine. Just not recover.
What does this difference mean? If you can run arbitrary function in your panic handler, what prevents someone from continuing the program?
davidjfelix
Unwrap is fine if used sparingly and as mentioned, to indicate a bug, but in practice it requires discipline and some wisdom to use properly - and by that I mean not just "oh this function should be a `Result` but I'll add that later (never).
I think relying on discipline alone in a team is usually a recipe for disaster or at the very least resentment while the most disciplined must continually educate and correct the least disciplined or perhaps least skilled. We have a clippy `deny` rule preventing panics, excepts, and unwraps, even though it's something we know to sometimes be acceptable. We don't warn because warnings are ignored. We don't allow because that makes it too easy to use. We don't use `forbid`, a `deny` that can't be overridden, because there are still places it could be helpful. What this means is that the least disciplined are pushed to correct a mistake by using `Result` and create meaningful error handling. In cases where that does not work, extra effort can be used to add an inline clippy allow instruction. We strongly question all inline clippy overrides to try to avoid our discipline collapsing into accepting always using `unwrap` & `allow` at review time to ensure nothing slips by mistakenly. I will concede that reviews themselves are potentially a dangerous "discipline trap" as well, but it's the secondary line of defense for this specific mistake.
wiktor-k
> That section briefly described that, broadly speaking, using unwrap() is okay if it’s in test/example code or when panicking indicates a bug.
I've come to the conclusion that even in tests unwraps look ugly. Especially in doc tests which serve as examples the code is better off using regular "?" that one would see in other parts of code. (that is: don't treat test code as a "worse" kind of code)
In Signstar we're using testresult which still panics under the hood (which is OK for tests) https://gitlab.archlinux.org/archlinux/signstar/-/blob/main/... but the rendered example looks a lot better: https://docs.rs/nethsm/latest/nethsm/enum.OpenPgpVersion.htm...
Note that I'm currently maintaining that crate but the design is described at https://www.bluxte.net/musings/2023/01/08/improving_failure_...
skohan
I always felt unwrap is preferable for diagnosing issues, which makes it useful in tests. A failed unwrap will point you directly to the line of code which panics, which is much simpler than trying to trace an issue through many layers of Results.
If you use `assert` in tests, I don't understand why you wouldn't also prefer unwrap in this context.
I think it's also perfectly reasonable to use in a context like binary you run locally for private consumption, or for instance a serverless function which allows you to pinpoint the source of errors more easily in the logs.
It's not a good fit for cases where you do expect cases where the unwrap will fail, but it's a great fit for cases where you believe the unwrap should always succeed, as it allows you to pinpoint the cases where it fails.
tialaramex
I call unwrap a lot in unit tests where it feels obvious to me that this succeeds, so, if it doesn't that's a huge mistake and should definitely fail the test but I have no pre-existing description of what's wrong, it's just a big red flag that I'm wrong.
Fallible conversions that never fail in the tested scenario are an example - parsing "15/7" as a Rational is never going to fail, so unwrap(). Converting 0.125 into a Real is never going to fail, so unwrap(). In a doctest I would write at least the expect call, because in many contexts you should handle errors and this shows the reader what they ought to do, but in the unit test these errors should never happen, there's no value in extensive handling code for this case.
wiktor-k
> I always felt unwrap is preferable for diagnosing issues, which makes it useful in tests. A failed unwrap will point you directly to the line of code which panics, which is much simpler than trying to trace an issue through many layers of Results.
Take a closer look at testresult since it also points directly at the exact line of failure (due to panics being used under hood) but looks nicer.
afavour
I agree that unwrap looks ugly. Sometimes I wish Rust had a ! operator like Swift and TypeScript do. But then that would encourage bad practises in non-test code, so…
littlestymaar
> but the rendered example looks a lot better:
For doctests it makes total sense, but for regular test no so much.
axegon_
I saw that article in 2023 and I have mixed feelings. In general there are cases where you want a program to crash and it's a preferable option as opposed to a never-ending chain of handling. For that purpose, unwrap is actually perfect. It is also great for examples, documentation, so that people can see what to expect and how a certain block should behave.
But there's the other case(which I've also suffered from). It is tempting to do the fast thing when you are on a tight schedule. One such instance was when we had to deliver something which was arguably undeliverable within the deadline we had. There were two services which were communicating over gRPC, and to make matters worse, not tonic but some custom implementation. But those worked surprisingly well against all odds. But there was a third service where we had to use a regular, plain ol' boring http requests. "I'm returning you the ID as a string inside the response, just cast it to a 32 bit integer and you're good to go" they said. "Fair", I thought: response_id::<i32>().unwrap(), compile and deploy. Needless to say, the ID was not a 32 bit integer. Point is, if you control the entire ecosystem you are working on, unwrap, although undesirable in many cases, is fine. Anything else - handle the errors.
epcoa
> It is tempting to do the fast thing when you are on a tight schedule.
The alternative option in C or C++ is to do some undefined behavior. Before a pile on, yes of course you can avoid it but the rush option is usually going to be the equivalent of unwrap anyway, and Rust does make it quite a bit harder to invoke undefined behavior.
voidUpdate
While I don't fully understand rust (not that I've really attempted to program in it much), I do really like the Result<T, E> convention. Before I knew about it, I used a similar construct in a couple of places in my C# code, but I think I'll make my own version of it and try to use it more often. I don't think C# has a builtin version anyway, I know it has a builtin that does the same as Option<T> with its nullable types, like int?, which can either be an int or null
metaltyphoon
C# may add discriminate union so you won’t need your own types! Both Result and Option should be included too.
HideousKojima
> I know it has a builtin that does the same as Option<T> with its nullable types, like int?, which can either be an int or null
int? in C# is syntactic sugar for the type Nullable<int>. The big issue though is that Nullable<T> only works for value types, not reference types like string (or any classes). C# added "nullable reference types" a few versions ago, but that is just static analyzers in the compiler and IDEs, it doesn't actually enforce anything at runtime. And all of the methods, etc. that are available for value types with Nullable<T> don't work with nullable reference types.
C# has union types as a planned feature, which will also add an Option<T> type that will make it possible to handle nullability for both value and reference types using the same type, and make it so it's actually enforced at runtime.
the__alchemist
Great article by a rust legend. Two points:
- The let-else syntax (Relatively new), anecdotally, removes many of my cases for unwrap. (But this may not apply to how others use it)
- Unwrap's fine if it makes the code easier-to-read and you know it won't panic for a reason the compiler doesn't know about.
vbezhenar
The general issue, as I see it:
One function is too general and handles all inputs. Some inputs are wrong and code returns error. Some languages forces you to handle that error.
Another code snippet uses that function with very specific inputs which must not cause errors, but was forced to handle error, because of language rules. This error handling is essentially useless.
I saw this issue with Java. There's constructor `new URI(String) throws URISyntaxException`. URISyntaxException is checked exception which must be handled at invocation site or propagated to the caller. But you might need to write code like `var uri = new URI("https://example.com/")` and any error handling code will only be to please the compiler, essentially wasted work. Java solved it using `public static URI create(String str)` which does not throw checked exceptions and you're supposed to choose either constructor or factory method, depending in the circumstances.
Using Rust analogy, may be it would be useful for Java to write something like `var uri = unchecked new URI("https://example.com/");`, so with minimal syntax overhead you would signal that checked exceptions are not supposed to occur here (and if they did occur, then some unchecked UnexpectedCheckedException would throw here). It's actually possible to implement method `static T unchecked(CheckedSupplier<T, E> supplier)`, though syntax would be ugly: `var uri = unchecked(() -> new URI("https://example.com/"))`.
I think that this is general issue for programming languages. There's only one way to validate input parameters and report error. But whether those input parameters are trusted or not - only the caller knows. So nothing wrong about using `unwrap()` to signify the fact, that caller already made sure that error does not happen. The only important nuance is that error must not be swallowed if it happened.
jstimpfle
It's a general issue with typed languages. Types are either too fine-grained or too coarse. Some more sophisticated languages can infer a whole lot but static type checking is still confusing and slows down builds. I tend to not worry much about them anymore -- about C language level types (maybe a bit more) is helpful and important for performance, but beyond that (e.g. types get dynamically instanciated from templates by the compiler) returns are diminishing quickly for many tasks.
The biggest robustness gains often come from making the right architectural decisions. That also applies to error handling as you described.
jerf
After starting my career in dynamically-typed systems for about the first 15 years, I'm pretty solidly in the camp of statically-typed systems.
However, I think it's really easy for a statically-typed-systems user to get a bit too enamored with static types even so, and start trying to stuff too much stuff in there. Anyone who thinks that the job of static types is to make all conceivable errors impossible is invited to go learn Haskell and then spend some time trying to write a non-trivial program, like, say, a GUI that also interacts with the network somehow, using an effects system to its maximum capability to perfectly carefully constrain every single function.
It is academically impure to do things like the author mentions and have a search object that will fail if you call a certain method without having constructed it a certain way. No question. And no question, this impurity can result in bugs in real code in the real world.
On the other hand, if one takes the time to create the absolutely perfect crate for Rust that absolutely perfectly expresses all possible combinations of options and methods that such a package could have, you could conceivably end up with a package with literally 10 times or more the number of types, that requires more steps to create a search than the simpler one, that is in fact so complicated that it turns the simple task of "please look for string x in string y" into something that a new programmer can't even understand anymore, and even a senior programmer might have to fight through some docs and ultimately just copy/paste an example and hope for the best...
... and the designer must ask the question, is that actually better? In practice, and not in theory?
Bugs are not created equal.
The academic worldview implicitly accepts the premise that an infinite amount of work is worth doing to eliminate the smallest possibility of the smallest bug with the smallest consequences.
A coder who lives in the real world needs to examine that premise carefully and decide if it matches their current situation, and if as is quite likely it does not, take appropriate actions. Static type systems offer a fantastic cost/benefit tradeoff in many real-world situations, but there is a point of diminishing returns.
im3w1l
I think the "correct" way to do it at a language level is for the compiler to construct the URI at compile time when possible. The successful construction proves that it cannot throw exceptions and hence they don't need to be handled.
johnisgood
I do not think a code full of unwrap() (which I have seen often) is a nice thing to look at.
goku12
That's not what the article is saying, despite what the title implies. He is saying that while unwrap should be generally avoided, there are a few genuine use cases for it.
I follow his advise on error handling, since that's what I found convenient as well. Rust error handling is hard to get started with. But you need to learn it only once before it becomes a second nature. Most of my projects contain only a handful of unwraps.
gwbas1c
My biggest beef with "unwrap" is that it doesn't contain an error message. Because I generally follow the mantra that panics are for unrecoverable errors, (typically bugs,) some kind of error message is critical in debugging the situation. (Because no program should fail with "mystery error".)
I went back to https://doc.rust-lang.org/core/option/enum.Option.html and I can't seem to find an alternative to "unwrap" that includes an error message. I vaguely remember finding an alternative to unwrap with an error message, but because I don't use Rust on a daily basis, I can't seem to remember how I did it.
ChristianJacobs
You're looking for `.expect()`, available on both Option and Result.
0l
Perhaps expect() would work?
perrygeo
unwrap() is fine for experimental and WIP code. But it's a code smell for production. I usually do a git grep for unwrap and go one of two ways:
1. convert to an expect() - even if a panic is fine, you at least owe the future reader an explanation.
2. Use "proper" result handling, usually let Some, .ok_or, or a match.
Unwrap is a nice ergonomic cheat code to get the program compiled and leave a little TODO for yourself. You just need the discipline to circle back and clean them up, just like any debugging tool.
Filligree
Just don’t put unwrap in library code, please. Not if it can possibly happen, bearing in mind that you can make mistakes.
I like my background threads to be infallible. If they panic, then chances are nothing will notice and the rest of the program will solely seize up.
I might have agreed with more of this article if panics actually crashed the program, but in multithreaded Rust that’s rarely the case.
goku12
I believe that your argument is implied in the article. Errors that are expected at runtime must be handled (preferably in the application code), instead of unwrapping it. If the thread does panic due to an unwrap (even without crashing the main thread), it's a logical error in the library that must be resolved before the public/production release.
There are cases where unwrap may be necessary even within a library. Burntsushi's own regex crate has an example of this. You get a Result when you construct a regex - which is reasonable if the regex is parsed at runtime. But I don't see any reason to not use unwrap, if the expression is a static string within the library source.
Zambyte
> I might have agreed with more of this article if panics actually crashed the program, but in multithreaded Rust that’s rarely the case.
Why is that?
jerf
The more threads you have doing something, the higher the probability you have at least one thread doing something that you don't want to have interrupted just because some other thread, somewhere, failed.
The lock case mentioned by littlestymaar is an important "program correctness" consideration, but also consider something like a instant message or chat server like Slack. You don't want the entire server with thousands of connections to crash just because one thread somewhere crashed. It's expensive to reconnect the system after that.
Also, the more threads you are running, the higher the odds get that one of them will panic somewhere, somehow. The numbers start adding up.
Each of these two effects conspire to make the other one worse as you scale up.
tayo42
I don't think this is uniquely a rust thing. I had a multi threaded python app that had uncaught exceptions kill threads and the whole thing stopped working but kept running along
littlestymaar
If you don't join the threads a panic get stopped at thread boundaries (unless you have taken a lock when the panic happens, in which case the lock is poisoned and attempting to lock it again will return an error. Error that will most likely trigger a further panic since you usually write something like
let mutex_guard= mutex.lock().expect("poisoned lock");
m00dy
I only use unwrap in tests
Hamuko
I use expect() so that I can grep for instances of "unwrap()" after the prototyping phase without getting tests.
theodorton
You can add
[lints.clippy]
unwrap_used = "deny"
to Cargo.toml if you want to avoid unwrap and then explicitly opt-in with #[allow(clippy::unwrap_used)]
The article pretty much says this as well, but a concise way I saw Andrew Kelley put it recently in the context of Zig (but it seems to apply well for any language that has errors-as-values + panics) is: "Assertions for programmer mistakes; errors for user mistakes."[0]
One interesting difference between Zig and other languages with similar error stories (Rust, Go) is that panicking can be handled but not recovered. Panicking is always fatal in Zig. The nice thing about this is that you cannot use panics for exception-style control flow, which removes any temptation to use them for user errors.
[0] https://ziggit.dev/t/i-wrote-a-simple-sudoku-solver/9924/12?...