Hacker News new | past | comments | ask | show | jobs | submit login
Rust should not have provided `unwrap` (thecodedmessage.com)
54 points by goranmoomin on July 15, 2022 | hide | past | favorite | 127 comments



Maybe it shouldn't have but developing in Rust would be intolerable without .unwrap(). In the real world you wade through API documentation figuring out the types and colors of functions while forever being assaulted by Result<>. Without .unwrap() your progress would be destroyed as you'd have to immediately deal with every unhappy path. unwrap() allows you nail together the happy path and then revisit the failure modes after you get your intentions working.

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.


The downside is that the "revisit the failure modes" step never happens.

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?").


> The downside is that the "revisit the failure modes" step never happens.

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.


Programmers who comment out Wall would be even less likely to use Rust if it didn't provide unwrap().


Pretty much all of my non-leaf functions return Result, in application code (or where I can't be bothered to build error types as I go) I'll use anyhow to allow me to coerce pretty much anything into my result type. So unwrap can -- fairly easily -- be replaced by `?`.

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.


> forever being assaulted by Result<>. Without .unwrap() your progress would be destroyed as you'd have to immediately deal with every unhappy path.

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?


Rust has std::Result<>, concise operators that comprehend that type, and various popular Result<> wrapper libraries that make the simple cases concise and pleasant to deal with. I love Rust error handling; it is among the best features of the language.

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.


That would be the ? operator, I think. It will return the error or continue execution with the data.


> Also, .unwrap() is fine for unit test mocking you need in real world code.

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.


What I have in mind is when your unit test scaffold must populate some collection or whatnot from a file that's part of the code base, or similar legwork. The read might fail somehow but if it does you have bigger problems, so .unwrap() is fine.


That’s why exceptions are superior, in my opinion. Rust’s unwrap is still better than Go’s case, because at least it won’t just silently continue, but all things being equal, exceptions are really great, I don’t get this newfound hate for them.

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 because people don't generally understand them. Especially checked exceptions are very difficult to understand if the programmer has not been taught the fundamental principles of how to deal with them (generally, don't, and just let them ripple up by marking your method as throws). Well that, and poorly designed API's in common libraries that forces the programmer to do bad things.

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).


Exceptions don't work well in systems languages. That's why, for instance, Google doesn't allow C++ exceptions[1]: "We do not use C++ exceptions." Full stop. Mozilla also follows this policy in Firefox[2]. The moment you start unwinding stacks over calls that are not aligned with your runtime's exception handling regime you're in crazy land and everything goes to pot. This includes all transitive calls however deep they go. Turns out that's unworkable.

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 explicitly wrote that Rust being a low-level/systems language gets a pass on exceptions.


Respectfully, I didn't reply to you.


I feel like the author is only thinking about high-quality production code. Often you're just writing a 30-line snippet of throwaway code to test something out. And then unwrap() is a bit nicer than expect("").


My thoughts exactly. Without panic people (myself included) would just write expect("") when we want the behavior of unwrap().

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.


For me, the problem isn't `unwrap` vs. `expect`, it's that I can't statically guarantee that particularly mission-critical code never panics.

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.


> 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.


I am in no way agreeing with the idea that `unwrap` should be removed. I simply wish it were possible to assert statically at compile time that my application will not panic.

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.


Being able to statically assert your program does not panic is predicated on being able to detect infinite loops. For example a panic could be detected to occur after a loop which may or may not be infinite. If you can't detect infinite loops you also can't detect if the program will panic or not.


Why the insistence that this has to be perfect for it to be of any use?

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.


Disagreed, we can have good sanity checks while forgoing checks for infinite loops.

It should not be an all or nothing affair. Improvements can be gradual.


What exactly is gradual about the question: "Can this program panic?". Either you are optimistic and allow any unproveable panics through and now you program will panic in practice. Or you are pessimistic and basically disallow any practical program.

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.


I was addressing your odd fixation on not being able to prevent an infinite loop. That's not a real downside. We can still make progress in many other areas in the meantime.


It's not about about being able to prevent an infinite loop. It's about being able to say this program really doesn't panic. Which is a very important property. For example the Linux Kernel requires Rust code to not panic. There was a lot of work done to ensure this was at all possible. And it wasn't to try and statically assert whether a piece of code panics. It was just disallow panics outright. Do you think these compiler developers just took that route because it's fun? Or because they are "oddly fixated on the halting problem"?


I don't know why you are so combative and immediately used the opportunity to reframe what I said above into attacking a good reasonable problem that we would all like eliminated (the halting problem) but I'll ask you to stop because that's not discussing in good faith; you seem to be on the lookout to attack someone and I am seriously not interested in that.

--

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?


I'm mostly writing "high quality production code" - and for logic errors which can't be gracefully handled I find little to no difference between .unwrap() and .expect(). What I'm usually mostly concerned about is understanding where the error happens if the application crashes, and a stack trace is the best way to obtain this information - not the actual text message. From a crash report and a stack trace it's usually clear what happened, and I think using either a custom panic! call or except would only be helpful if more information about the current state of the program would be helpful to debug the issue (like "expected value to be between 127 and 255, but not 1234").

For errors that can be gracefully handled - sure, using ? is definitely preferred to any kind of panicking.


It's as if people don't read the article before commenting, because the author mentions exactly this use case too:

> 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.


What about extremely trivial expected conditions that occur all over the program? "expect()" would get tiring pretty quickly.

  Regex::new(r"\bstruct\b").unwrap()
  // vs.
  Regex::new(r"\bstruct\b").expect("bad regex string literal")


> 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).


Except when you want to compile a regex at runtime from some input (like a config file for example).


But in that situation you'd want a Result because it absolutely could fail at runtime. And you wouldn't want to use unwrap on that because user input could tank your program.

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.


In this case at least it seems like one solution is just compile-time function evaluation?

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.


Regex used to offer a regex!() macro that did compile-time compilation, but this was removed a while back as it was too much of a maintenance burden and was getting in the way of performance optimization.


Well, there we go, then. Nobody wants to do the work.


IIRC it required basically two separate implementations of the data structures, one that could be constructed at compile-time and was less efficient than the one that could be built at runtime. And the need to support both was becoming a problem.

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 author encourages something like:

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")


Sounds very similar to annoying Java checked exceptions for stuff like 'new URL("http://example.com")' -- it can't fail, but you have to catch it.

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).


You want the "?" operator.


No, because propagating an error due to an incorrect static string to the caller is not the right thing to do.


Isn’t the point that in this case it cannot ever error because the string is static?


Yes, but if you use the `?` operator, as the comment I replied to suggested, then you have to return a `Result`, which the caller has to unwrap themselves or otherwise handle. Except the caller doesn't have this context about how it can't be an error because it's a static string. Propagating the (impossible) error up the call chain is the worst of all worlds in a case like this.


`unwrap` is fine. As the author mentions, it can be converted into `?` without much hassle if the function return type can be changed easily. And `?` looks cleaner than `unwrap`, so the incentives are aligned to nudge developers towards that instead.

`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.


Sometimes you don't want to use `?` though, not every function is made to return a Result / Option.


If your function is fallible, your function is fallible - it's up to you to decide whether that means returning Result or panicking. If you want your function to be infallible, while itself calling a fallible function, you have to handle the error cases from your callees - you have `.unwrap_or_default()`, `.unwrap_or_else()`, `.unwrap_or()`, and simple `match` all in your toolbox to do this.


In that case, you use `unwrap`, or you manually convert it into your return type, or you provide an `impl Try` [0] for your return type once that feature becomes stable (already possible if you're on nightly)

[0] - https://github.com/rust-lang/rust/issues/84277


The footgun here is that `unwrap` may be used inappropriately, and from what I see using github code search it's used inappropriately quite often.


So is `panic`, or the subtraction operator on unsigned values which may cause underflow.




"Looks like you're having problems enforcing no unwrap. Would you like help? [Yes][No]"


I tend to agree with preferring alternatives (?, Expect, unwrap_or_else) to unwrap in most cases, but I do still find myself using unwrap in cases where it's basically impossible for it to fail. e.g. imagine a parse method that returns a Result in case the input is not a valid whatever, but I've just called it with a literal value.

At the same time, looking at one of my codebase, 90% of such cases are in my test code


I agree with you. My general rule of thumb is that I use `unwrap()` for cases where the panic should be unreachable, and is at least reasonably easy to prove it's unreachable.

Otherwise, `?` in 90% of code, with panics far up the stack, e.g. in `main()`


> My general rule of thumb is that I use `unwrap()` for cases where the panic should be unreachable, and is at least reasonably easy to prove it's unreachable.

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.


> 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

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


So an ad-hoc way to do the exact same thing requiring doubling the size of your api? Is your problem with unwrap that it’s too easy to find?


Ugh.

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.)


The standard method name also makes it super easy to go back and tighten things up when the time comes

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


It's also something you can do static analysis for with tools, and I imagine the compiler can be adjusted to warn or disallow its use in modules and dependencies.


There is a problem here - it's verbose and prone to silent-ish breakage if the surrounding context changes e.g. due to refactors (well, probably not with this tiny example, but I hope you know what I mean). Thankfully, there is also a solution to this problem: https://github.com/rust-lang/rust/issues/90091

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))
        }
    }


> [...] well, probably not with this tiny example, but I hope you know what I mean [...]

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.)


That's helpful for that one example, but it doesn't solve the general case. I can think of a hundred different examples that aren't handled by the type system or stdlib.

  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.


It's also the case that this call to `unwrap()` will be removed by dead code elimination.

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.


In this example you could do: (n >> 16) as u16

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.

https://doc.rust-lang.org/std/convert/enum.Infallible.html


Maybe the function unwrap should be renamed to ISolemnlySwearThisCanNeverFail.


We'd need an even more unholy name for `unsafe { foo.unwrap_unchecked() }`!


That’s a feature!

I’d be thrilled if C++ required wrapping declaration and use of global variables with ‘unsafe_and_evil { … }’


I do appreciate that Rust tends to have the mentality that they will, after careful consideration, add something like split_array to solve X ugly problem.

But it leads to just a TON of features and approaches to learn.


I think the idea is that `expect()` accomplishes the same thing, but allows you to put an explanation of why the function should be total (such that if it does indeed panic, you can see what went wrong with the reasoning).

That said, I do think there's a place for "unwrap" in eg. tests.


That seems even more verbose, for a simple function that is already very verbose to begin with, given how little the function does.


I find 9 times out of 10, the source location of the panic (main.rs:123) is enough for debugging, and the .expect() text is redundant. I would never advocate .unwrap() for production services where other people may have to debug the code years after you write it, but there are many, many other use cases for Rust.


If we truly want to get rid of unwrap() in most cases, and not have the language be a bear* to use, then it needs a tactic language or something for proving invariants like that at compile time.

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 ;)


We might not be able to always reason about function totality, but perhaps sometimes we can. Isn't that what `std::convert::Infallible` is trying to do? So you get

   fn try_from(value: U) -> Result<Self, Infallible>
In which case you could potentially avoid the need for an unwrap.


Could you give a more complete example? I see the type signature for the function you're proposing, but I still don't understand what is supposed to replace .unwrap() on the inside of the function.


You'd pair it with something like this https://docs.rs/unwrap-infallible/


I don't see how that would work.

    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.


In jacobr1's comment we already have a way to get a Result<_, Infallible>. But converting a slice to an array returns a real error type, so my comment wouldn't apply.

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.


The "fix" would be to return a Result. Which is a sum of an error and the type you want to return.


But again, the function cannot fail. It could return a Result<Vec2, Infallible>, I guess, but then you're just pushing the unwrap to caller without any need to.


That's the entire point of the OP. That it can't fail so you might as well call unwrap.


You can't return an Infallible, though, because it has no variants. You still need to do something at the point of unwrap().


I couldn't agree more. If course it would be nice of we could prove everything. It would even nicer if we would have a compiler that is smart enough. But it's just not practical for a lot of cases.


TLDR; - While I agree unwrap/expect should not typically be used often in production code there are enough exception cases that Rust would be MUCH worse if they didn't exist

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 agree unwrap is tragically overused in example code, and any non-logic error should use a Result instead — even early in a project. This isn’t a huge overhead. Even in “prototypey” code, it’s not hard to make everything return an anyhow::Result and upgrade to more fine-grained error types later. The main function can return a Result as well (which will be unwrapped by the runtime), making this style almost as concise as the unwrap version.

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!().


It’s still useful in cases where you’ve already handled the error case. For example:

  if res.is_err() {
    // Do something
    return;
  }

  let res = res.unwrap();


You can replace this with a match statement to avoid the unwrap.


It does introduce another layer of nesting in the code, which can be undesirable from a readability perspective at times


Not necessarily, you can extract the inner type by using the match as an expression, e.g.:

``` 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.


Rust is getting let-else to solve that problem: https://rust-lang.github.io/rfcs/3137-let-else.html


Ahh, looks nice, here's hoping it gets stabilized


Fwiw there’s a small crate called guard which has provided this feature for several years.

I heartily endorse it.


Wow, this would definitely make it much cleaner!


Of course, but I think this approach signals intent a bit more clearly: we want to get error handling out of the way and then just proceed with the happy path. It’s also a tad less verbose.


I'd use a 'if let' for something so short.


This is a good use case for let-else, however I won't agree with you here on usage of unwrap for the sake of brevity. I will opt for `?` or match instead.


In the Objections section the author concedes that some API calls most commonly fail on logic errors, like creating a regex, and writing an error message for each of these would be unnecessarily verbose. Their counter to this argument is that there should be two versions of that function, one that panics automatically (and that you're going to use 95% of the time) and another that returns a Result (e.g. for user-supplied regex).

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.


I feel mixed on this, it's kinda like arguing that the Hello World for Java needs to include classes because it starts people off on the right foot.

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.


[OT] Reading this article makes me want to use Rust so badly. I just don't have any places to use it. Everything at work is either Go or Scala or Java. Or front-end shit I'm not smart enough to do anymore. I want to find a job that'll pay me senior engineer rates but let me learn Rust like a baby and eventually do something useful with it...


> I want to find a job that'll pay me senior engineer rates but let me learn Rust like a baby and eventually do something useful with it

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.


Clippy has a lint that will check for `.unwrap()` calls; https://rust-lang.github.io/rust-clippy/master/index.html#un....

It is allowed by default, but you can turn it on in the configuration file.


I'm not familiar with Rust, but it sounds like a similar story to `?`, `!`, `rethrows`, and `fatalError` in Swift. I.e.:

- 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.


I am not convinced that propagation is massively superior to unwrap in example code. Unwraps are nice because they give you error messages with line numbers, but line numbers don't seem to come for free with propagation.

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


Unwrap is just the same (or better) than expect(""). The cat is out the bag with expect. What is needed, is good documentation, style guides, example code etc to guide users towards understanding the trade-offs and making informed decisions. I suppose unwrap could have had a more scary name.


Yes, we should have unwrap. Rust would move towards insufferable if it did not — sort of like Go and its “you must use every variable” thing.

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.


I broadly disagree, but others here have covered all the points I would have made around that, so instead I'll mention an interesting thought this did bring up for me:

> 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?


If a keyword is here, generally, it means there is a use for it. Feel free not to use it, but it is generally a better idea to try to understand why it is there before calling it a mistake.

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.


See also the discussion for a similar feature in Java, from "Optional: The Mother of all Bikesheds", where the use and misuse of get() has a central role.

video: https://www.youtube.com/watch?v=Ej0sss6cq14

slides: https://stuartmarks.files.wordpress.com/2017/03/optionalmoth...


Ok, that argument is about as old as Rust. With one difference that now the "?" operator exists.

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.


When I was learning Rust, unwrap felt like cheating


It is! :) Perhaps the name could have made it more obvious: e.g. "get_or_fail"


It could be named .ordie() like php idiom from olden times.


I associate it more with Perl. A quick search reveals Perl to be older than PHP by 7 years, so I'd guess it's a Perl idiom borrowed by PHP.


I'd also be happy if it was removed. It's too easy to use it rather than going up the learning curve of ?.

? Is one of the main benefits of rust.


I tend to agree.

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...


Your example doesn't help your argument here, as square should be taking `T` instead of `Option<T>`, a good use case for `Option::map`.

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.


For the specific examples you have here you can shorten it a bit and use anyhow's OptionExt::context and avoid the closure. Someday maybe we'll have postfix macros and you'll even be able to format in it.


You can use a "?" on an option if the return type of the function is also option.


I don't see how removing `unwrap` like the author dreams of would result in anything other than a lot of `expect("oh no")` instead - the problem refactors itself from "sloppy developers use unwrap which has a generic panic message" to "sloppy developers use expect with a generic panic message".


My only issue with expect is it seems like the wrong name. It sounds like it means "I expect this condition to be true" whereas it really means "if this thing is not ok, print this message and panic".

Why not just call it unwrap_or_errmsg?


As I said in the past, I'd love a a compiler flag / Cargo.toml setting that disables all panicky Rust stdlib API. Obviously many people don't need or want that so it should be opt in only for those that do.


Using a mutex basically requires the use of unwrap

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.


Looks like an expect there would be an improvement. Do you write that line often enough that adding an error message is a problem?


The reason I would hesitate to replace this with an expect is that I really don't ever expect .lock() to fail, so .expect just adds verbosity for no benefit.

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


There's not really a benefit to "except", since the stacktrace you get on the panic tells you exactly what happened (Mutex was poisoned). Repeating that in a custom error string doesn't make the behavior much better.


My small gripe with unwrap is that the name is too similar to the perfectly safe unwrap_or (and friends). This is annoying both when reading the code and when grepping.


TLDR: Industrial-strength code should not have "mystery error" failures. Always include some kind of error message; or adopt techniques to abstract away providing the error message.




Join us for AI Startup School this June 16-17 in San Francisco!

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: