Rust should not have provided `unwrap`
thecodedmessage.com
thecodedmessage.com
Also, .unwrap() is fine for unit test mocking you need in real world code.
Are irresponsible and/or under-resourced programmers abusing .unwrap()? Yes. Obviously. Fortunately, the frictionless .unwrap() makes this blatantly obvious to everyone else (just as profligate use of unsafe {} does,) so detecting crap code is much easier. This is a vast improvement over traditional 'systems' languages that permit said programmers to ignore failures without so much as a word.
The net result is .unwrap() is a win for Rust.
People keep saying this but it is sometimes incredibly frustrating how little people annotate their test failure points in rust unit tests. I would really like to see more people get a little verbose in their tests and use the third arg of the assert macros and .expect with explanations of what the line is trying to prove.
I'd still like to be able to agree with the compiler that it can prove that certain error cases don't happen, as with ether an unthinking propagation upwards or an unwrap I'm reliant on my own analysis to assure myself I don't need to explicitly special-case that error. If the compiler is able to dead-code eliminate the unwrap, I'd like to be able to tell the compiler that there's a bug in my code if the unwrap is still in place after optimisation.
I've worked on C++ projects with a programmer who commented out "-Wall" with the comment "No one has time for that" (and then I enabled it and spent a couple days with valgrind tracking down crap that should never have happened). I remember the hate for Java's checked exceptions ("Convert them all to unchecked exceptions and forget about it." -> "Why is the app blowing up in production?").
You exaggerate, but yes crap code exists. As I said, however, you can spot it from across the room with grep -r 'unwrap()'. That's all I ask; can work be evaluated easily? Rust doesn't guarantee correctness; thus unsafe{} pragmatically exists. Rust just makes much incorrectness more obvious and easier to detect, both for the coder and for those upon whom their work is foisted.
I don't know much about Rust but eg. in Haskell you run a Result-returning computation in its monadic context where success continuation/failure propagation is taken care of by the underlying machinery (ie. the implementation of bind), eg.:
f :: Either Error Stuff
f = do
foo <- getFoo ...
bar <- getBar ...
...
Does Rust have something similar?BUT! When dealing with nontrivial cases, particularly with asynchronous code, Result<> can get complicated. Not everything is Copy. Sometimes you have to type erase errors. That chore tends to be a bit of a hairball, as the many StackOverflow cries for help will attest. Other Result<> hairballs exist as well. For one thing it's a bit anemic in the 'original cause' department and sometimes you need to elaborate on it to capture more information about failures.
Coping with these cases is most comfortably done after you've nailed down your intentions and shown yourself that your code and whatever mass of dependencies you're reusing work as intended. Later, when dealing with the failures you papered over with .unwrap() you might end up doing some refactoring. At that point, however, you are confident that your effort will ultimately work.
The brilliance of .unwrap() is that your technical debt is visible. You can spot it in the dark, as can everyone else. In my mind that's a killer feature of Rust. I can't make you write good code, but at least I stand a chance of spotting it when you don't.
They are analogous to Result types, just supported at a deeper level, with better default behavior (auto-unwrap and bubble-up) and better debugging (stack-traces).
Though in Rust’s case, I think not having exceptions was a good choice (due to it being a low-level language)
It's very common that the fashion opinions of the day decides that something is bad or suboptimal, and the community reacts by doing the complete opposite, giving us something that may not have the core issue of the original problem, but also lack all of its advantages. A few years later the pendulum then swing back in the other direction.
Examples include the sequence: mainframes (server), desktops (local), webapplications (server), SPA's (local).
Rust designers understood all of this. Exceptions in systems languages are a misfeature that beguile the naïve.
Is there some glorious future system where this is no longer the case? I don't know. Microsoft tried and made an inscrutable mess of it that C++ programmers bang their unhappy heads upon daily. Apple's objective-c had to adopt the C++ ABI for exception handling creating its own proprietary hairball.
No ones done it well yet! The wise find that informative.
[1] https://google.github.io/styleguide/cppguide.html#Exceptions [2] https://firefox-source-docs.mozilla.org/code-quality/coding-...
I have some Rust code that I'm the only user of; in those cases I'd rather investigate an error when it comes up than spend time upfront thinking about errors that may never happen.
I can be as careful as I want in writing panic-safe code, but there's no ergonomic and non-hacky way to ensure that I don't use third-party crates in a way that can possibly panic either.
There's also no way to ensure those third-party crates don't enter an infinite loop; at some point, if you need to guarantee your program doesn't diverge, you need to either trust the libraries you're using to behave reasonably, or you need to audit them yourself. Removing `unwrap` wouldn't solve that problem, and given how easy it is to lint for, it doesn't even meaningfully contribute to its difficulty. As the GP and some of the other top level comments point out, `unwrap` is a genuinely useful tool to have in the std lib, so advocating for its removal (or that it was a bad choice to begin with, presumably hoping that future languages will avoid including amenities such as `unwrap`) seems like a poor design trade-off from where I stand.
Being unable to also detect infinite loops does not diminish this utility. I simply wish to reduce the likelihood that my mission-critical programs diverge as much as is possible. Being perfect is not a requirement.
If the optimizer can prove it isn’t used and elides any code that might call it, great. If it can’t, I can rewrite my code to not use that function or call it in a way where the optimizer can make that guarantee.
There are all sorts of useful things that technically depend on deciding the halting problem, but turn out to be pretty easily solvable for virtually all practical cases.
It should not be an all or nothing affair. Improvements can be gradual.
There are a lot of cases where a gradual approach works well. But something so fundamental and important like "Can this program panic?" is not one of those cases.
--
Having said that: being able to disable / remove `.unwrap` and co. with a compiler flag -- as you seem to imply is the case in the Linux kernel branch of Rust -- would prove hugely beneficial to no small amount of library and app developers.
And guaranteeing no panics isn't that hard. Sure the POSIX standard and Linux in particular are vague in many places but I think it's fairly reasonable to be able to guarantee that a program that doesn't use filesystem or network can never panic just through dependency analysis (e.g. no API is used that can propagate a panic from the kernel).
How useful would that program be is another discussion entirely though (quite an amusing one at that).
My point was and remains that not being able to detect infinite loops is not indicative of the ability to prove that panics [due to system / kernel API carrying them over to you or just straight up blowing up your program] will not occur. And you seem to claim the opposite?
> 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.
For errors that can be gracefully handled - sure, using ? is definitely preferred to any kind of panicking.
Regex::new(r"\bstruct\b").unwrap()
// vs.
Regex::new(r"\bstruct\b").expect("bad regex string literal")Due to the literal, hypothetically that Regex::new's result can be pre-computed and the result encoded in the output binary, not the initialization.
I feel like there should be a middle ground here, a macro that validates at compile-time but still constructs at runtime. Hopefully the parser could still be shared between the macro and the runtime to ensure the macro can't get out of sync.
The industry dealt with that by using your own utility functions like "create an Url and convert exceptions to runtime ones" (Java folks don't focus that much on quick pet projects, so don't mind extra work).
> 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).
I think it would make sense for there to be some kind of compile-time macro for regexes. If the regex is based on a constant string then it should be a build error when that regex isn't valid. And the existing API (result, unwrap and all) would work great for non-constant strings.
Regex::new(r"\bstruct\b").some_custom_function_that_calls_expect()
or
Regex::new_or_panic(r"\bstruct\b") // Just calls Regex::new(r"\bstruct\b").expect("bad regex string literal")
`unwrap` is plenty explicit, and developers know exactly what it does (and that it might panic) once they've spent a few hours with Rust. It's not the subtle footgun the author is making it out to be.
At the same time, looking at one of my codebase, 90% of such cases are in my test code
Otherwise, `?` in 90% of code, with panics far up the stack, e.g. in `main()`
I don’t agree with that. To me unwrap straddles the line between “I don’t care to think about this right now” and “I don’t care about that case”.
If I can prove it does not happen then it get an “expect” with a justification. Or even a map_err(|_| unreachable!(reasoning)) if I’m feeling really fancy.
unwrap acts as a todo, I can grep for it and if I see one I know it’s something to fix.
I like the Go stdlib idiom for this: many functions have a version with a "Must" prefix that has the behavior of panicking rather than returning an error.
e.g. for regexp: https://pkg.go.dev/regexp#MustCompile
The basic argument is something like "all functions should be total", but the problem is that proving that a function is total is really annoying, so we live with two other consequences instead:
1. Some functions are not total, and you just have to know the preconditions.
2. Some functions are total, but the type system is not powerful enough to prove this (possibly because they call functions in category #1).
The same discussion has been going on for decades in Haskell-land, and there's no definitive consensus, but my sense is that we'd rather tolerate a panic here and there rather than try and deal with the fuss of propagating errors or proving that our functions are total in the first place. For example, take a look at this function. I ask,
> Is this a reasonable way to write this?
struct IVec2(i32, i32);
impl IVec2 {
fn parse(d: [u8; 8]) -> Self {
Vec2(i32::from_le_bytes(d[0..4].try_into().unwrap()),
i32::from_le_bytes(d[4..8].try_into().unwrap()))
}
}
Someone from the Rust community will come in and say,> Arguably, unwrap() is unreasonable.
...but why? The function is total. It won't panic. The response is,
> ... an error should be properly handled ...
There is no error, the code can't fail. I don't know of another way to write the function without a panic hidden somewhere, either in an unwrap() or inside an indexing operation. For some reason, some members of the Rust community think of this as a problem to be solved.
It's not a problem. The unwrap() method is just too damn useful. It's too damn useful to be able to write functions that are not total. I'd say that the key here is coming up with a sensible style for when you are okay with panicking in your code, and then accepting the consequences of that decision.
("Total" means that the function returns a result for all inputs. Unwrap is not total. In Haskell, "head" is not total--it returns the first element of a list, but throws an exception if the list is empty.)
That said, I do think there's a place for "unwrap" in eg. tests.
fn try_from(value: U) -> Result<Self, Infallible>
In which case you could potentially avoid the need for an unwrap. d[0..4].try_into()
The type of the result is something like: Result<[u8; 4], TryFromSliceError>
How do I convert that into Result<[u8; 4], Infallible>
As far as I can tell, Infallible is there because you're implementing a trait which returns an error, but you don't have an error to return at all.For your case just stick with `.try_into().unwrap()`. It's the easiest way. Unless of course someone on crates.io made a fancy crate with const generics.
Past that, we're going to have code that calls unwrap(), unless we want what should be simple to consist mostly of pattern matching boilerplate (and functions returning Result<T,...> when they could just be T).
* - Of course, now you have to deal with a tactic language / dependent types, and those could arguably be called a bear in and of themselves ;)
I recently had a nontrivial project where I'd used maybe a dozen unwrap()s in different places and had gotten a handful of bug reports for resulting panics. So I decided to make a pass and finally clean them up
Project-wide search for unwrap(), went through and properly handled all of them, very quickly and easily plugged all the holes, no more panics being reported
split_array will eventually allow something like the following, which avoids any explicit indexing and will fail to compile if the sizes don't match up:
struct IVec2(i32, i32);
impl IVec2 {
fn parse(b: [u8; 8]) -> Self {
let (first, second) = b.split_array();
IVec2(i32::from_le_bytes(first),
i32::from_le_bytes(second))
}
}But it leads to just a TON of features and approaches to learn.
Well, here's the thing. These small pieces of code DO show up in actual, real codebases! I've gotten feedback during code reviews along the lines of, "What if the surrounding context is refactored, this tiny, simple piece code could break" and to be perfectly honest, when I get it, I come down to your desk and we have a discussion about whether that kind of feedback is appropriate, and the purpose of code reviews.
(It also always seems like the issue I'm looking at is "fixed in nightly", but some of those features in nightly take a long time to get accepted, and the ergonomics of split_array() seem a bit dubious to me. Are you going to chain three split_array() for four fields? Obviously, for simple serialization and deserialization there are a ton of different options to automate this code away, but it's nice to be able to write simple code like this when appropriate, and the ergonomics of simple tasks matter.)
fn high_word(n: u32) -> u16 {
(n >> 16).try_into().unwrap()
}
Unless someone manages to solve the halting problem, we'll always need unwrap() or equivalent.I’d be thrilled if C++ required wrapping declaration and use of global variables with ‘unsafe_and_evil { … }’
See: https://play.rust-lang.org/?version=nightly&mode=release&edi...
Where if you look at the asm for playground::high_word then you'll see it's a bare shrl.
I'd quite like an analogue to unwrap() that's checked at compile time -- an out for the type checker, but an error if it's not dead code eliminated. This helps us to trust and rely on the compiler, and is the converse of `unsafe_unwrap`, which tells the compiler to trust us.
There is also this thing called "Infallible". Which will eventually be merged with "!". To express to the compiler and the programmer that something can never fail.
Longer version:
Only skimmed, but I disagree, although I thought the same thing when I started. It sounds wonderful to have panic-free Rust but when when you sit down and think about how that would work you quickly realize it is a trade off between boilerplate and convenience.
Rust code can be broken down into mostly three "types":
1. Safe, and can't panic (calls nothing that can panic)
2. Safe, can panic (.unwrap, .expect, indexing, etc.)
3. Unsafe, could do anything
The goal when writing Rust IMO is to try and get as much as possible in #1, control #2 carefully by closely held invariants, and avoid #3 as much as possible (in many programs, this turns into no unsafe at all).
The problem with avoiding #2 entirely is you will quickly get code that is hard to write, harder to understand, etc. Most often you will see situations like "I just proved this is x in length, so I can always index this with this value". If you don't do this you will have lots of error checking that effectively does nothing. I admit this happens rarely for Option/Result and in prod code I rarely use unwrap/expect, however, some code would be very tedious to write without unwrap or potentially panicking code, so unwrap/expect can be valuable for things like tests, examples, and rapid prototyping.
I should probably add that I actually agree with the author that in the _typical_ case, you should not be using unwrap/expect, but either pattern matching it or propigating it with ?, but the exceptions to this (testing/examples/prototyping/corner cases) are important enough that I'm very glad Rust has the "escape hatch" of unwrap/expect.
I also agree that panic and crash is usually the correct response to a logic error.
However, I think library functions like Regex::new shouldn’t decide for me what I do or don’t consider a logic error — how should the library know where that string comes from? They also shouldn’t be effectively required to offer two overloads for every function just to enable both decisions: there’s already a very clean way to put that decisions into the hands of the library user, Result.unwrap() vs. Result?.
Now, about unwrap vs. expect: Since the author and I agree that panics should be reserved for logic errors, the expect() argument is effectively only a debugging aid. Should that be required? I’m torn. Often, which one I personally use reflects my confidence in getting the invariant correct and never hitting the panic in production. That can be risky. A particular team in a particular context might therefore adopt a convention to always use expect instead of unwrap, that’s fine. But I don’t think it’s unreasonable that the language offers a choice here, and in similar places, like the no-parameter variant of panic!().
if res.is_err() {
// Do something
return;
}
let res = res.unwrap();I heartily endorse it.
``` let val = match res { Err(e) => { ... return ...; } Ok(v) => v, }; // Use val without unwrapping here. ```
That said, even though I'd avoid unwrap in this particular case in my code, I generally disagree with the author.
Taking out unwrap and leaving in expect would simply lead to a bunch of code that goes `let val = result.expect("!");`, which is equivalently bad when held up against the criticisms the author makes of unwrap.
Unwrap isn't really harmful so much as a symptom and an escape valve around the fact that our type systems really aren't powerful enough to derive all the invariants the programmer can about their code.
I get that argument, but imo that would be an even greater mess than having unwrap. It creates a burden for every library author to figure out how to handle errors, and to provide multiple options if necessary. The standard library goes this way sometimes, like with array access (foo[i] panics, foo.get(i) returns result) or println (println! panics, writeln! returns result). But the latter often catches people off guard because they don't expect it to panic. Propagating implicit panics to more places in the ecosystem in the name of getting rid of explicit panics with bad error messages (using unwrap) seems like a terrible tradeoff.
Unwrap is admittedly a stupid tool, and if you're starting off in Rust it becomes one of those "when all you have is a hammer, everything looks like a nail" situations. It's like a can-opener for fussy functions that want you to write semantic code, and it's admittedly quite good at what it does. Too good.
Like the author highlights, simply appending '?' to the end of your line is a better way to handle 90% of unwrap cases, and the 'panic' macro should cover most of everything else. Unwrap should really only be used when you disagree with the library developer about error handling. In all other cases, you should probably respect the error propagation of the program. Explaining all that to a new Rust user is hard though, so I don't really nitpick with it until people are comfortable with the memory model and control flow of Rust.
You and me both. I've been learning Rust, slowly but surely. I'd like to use it at work, but most companies I've seen require experience with it.
It is allowed by default, but you can turn it on in the configuration file.
- Handle nil cases with ? or propagate errors with rethrows.
- If you really, seriously can't recover, use fatalError with a descriptive message.
- ! is strictly for non-production code. It is just a hyper-short way of fatally failing with no message. Yes, maybe you can use it if you just checked for nil, but almost always you can refactor that to omit the !. Because of its convenience, people tend to abuse it.
Consider this code[1]:
fn main() {
let _my_num: u32 = "not a number".parse().unwrap();
}
Running this gives a slightly noisy/verbose message, but to my eyes it's pretty useful: thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: ParseIntError { kind: InvalidDigit }', src/main.rs:2:47
Compare this to similar code with propagation[2]: fn main() -> Result<(), Box<dyn std::error::Error>> {
let _my_num: u32 = "not a number".parse()?;
Ok(())
}
Not only do I find the extra boilerplate distracting for this example code, but if I wanted to run this code in a playground, the error message seems worse as it omits the line number: Error: ParseIntError { kind: InvalidDigit }
It's true that crates like anyhow[3] have robust support for producing backtraces (and I use them all the time in non-example code). But I'm not inclined to add an external crate to my examples just to avoid calling an unwrap().[1] https://play.rust-lang.org/?version=stable&mode=debug&editio... [2] https://play.rust-lang.org/?version=stable&mode=debug&editio... [3] https://crates.io/crates/anyhow
The way forward is a feature allows static checking of whether code paths could possibly panic so people can gradually mark their code as panic-free.
> Most of the time I see it, ? would be better and could be used instead with minimal hassle
I don't agree that you should just bring in a library, as a default strategy, to make it easy to use ? in all cases without thought
But. For the subset of cases where you could do this without any type changes - where the unwrapped error already has the right type for the containing function - I wonder if it would make sense to have a Clippy rule and/or IDE autofix?
Here, it goes well with what is called "offensive programming", a take on "defensive programming". Here the idea is that you crash as soon as something unexpected happen. It is a bug, fix it instead of trying to recover from something you don't understand. It is actually common in embedded software, if something is wrong, stop execution and wait for the watchdog to trigger a restart.
video: https://www.youtube.com/watch?v=Ej0sss6cq14
slides: https://stuartmarks.files.wordpress.com/2017/03/optionalmoth...
So, quick code that doesn't care about errors can be done quite simply by just using "?". The real place where there is a relevant use-case for unwrap is on code examples. And I am really not sure "?" is a good replacement for those, because there is a good chance that you will hit the people trying to learn from your example with a complicated type error before they even get some code to run.
? Is one of the main benefits of rust.
You can use "unwrap()" on an Option, but you can't use "?". You have to write something like
let val = v.ok_or(anyhow!("Error message goes here"))?;
which is a bit unwieldy. fn square(v: &Option<f32>) -> f32 {
Ok(v.unwrap()*v.unwrap())
}
becomes fn square(v: &Option<f32>) -> Result<f32, Error> {
let val = v.ok_or(anyhow!("None value sent to square"))?;
Ok(val*val)
}
A default message would be useful.Bailing out of the middle of a map expression's closure is complicated. In that situation, "?" causes a return of the expression in the closure, not the whole map. There's a clever way around this.[1] It supposedly gets optimized so that the iteration quits early, and an array of Result types is not generated and then flattened.
You never really need unwrap, but the "right way" can be verbose.
[1] https://stackoverflow.com/questions/26368288/how-do-i-stop-i...
But if you are speaking about ergonomics of handling Options, it is being improved upon with the usage of try operators with Options as well as possibly seeing try expressions in the near future.
I don't think lack of ergonomics is a good justification for usage of unwrap, but that is entirely subjective. However, I do see the misappropriation of `unwrap` as a signal that the ergonomics in Rust are lacking and it's something that needs to be addressed, so again I do sympathize.
Why not just call it unwrap_or_errmsg?
let lockedValue = mutex.lock().unwrap();
The only time .lock() can fail is if another thread panicked and poisoned the mutex, so propagating the panic makes the most sense here.
The article talks about the Regex::new method, which has much the same problem. For the majority of use cases, you don't expect it to ever fail. Maybe Mutex could introduce a new method .lock_or_die, because it's such a common pattern