[HN Gopher] Don't unwrap options: There are better ways (2024)
       ___________________________________________________________________
        
       Don't unwrap options: There are better ways (2024)
        
       Author : mu0n
       Score  : 82 points
       Date   : 2025-05-13 17:58 UTC (5 hours ago)
        
 (HTM) web link (corrode.dev)
 (TXT) w3m dump (corrode.dev)
        
       | ChadNauseam wrote:
       | let-else is awesome. definitely my favorite rust syntax. The
       | compiler checks that the else branch will "diverge" (return,
       | panic, break, or continue), so it's impossible to mess it up.
       | 
       | the article says "It's part of the standard library," which gets
       | the point across that it doesn't require any external
       | dependencies but it may be slightly misleading to those who
       | interpret it literally - let-else a language feature, not part of
       | the standard library, the relevant difference being that it still
       | works in contexts that don't have access to the standard library.
       | 
       | I tend to use Option::ok_or more often because it works well in
       | long call chains. let-else is a statement, so you can't easily
       | insert it in the middle of my_value().do_stuff().my_field.etc().
       | However, Option::ok_or has the annoying issue of being slightly
       | less efficient than let-else if you do a function call in the
       | "or" (e.g. if you call format! to format the error message). I
       | believe there's a clippy lint for this, although I could be
       | mixing it up with the lint for Option::expect (which iirc tells
       | you to do unwrap_or_else in some cases)
       | 
       | I appreciate the author for writing a post explaining the
       | "basics" of rust. I'll include it in any training materials I
       | give to new rust developers where I work. Too often, there's a
       | gap in introductory material because the vast majority of users
       | of a programming language are not at an introductory level. e.g.
       | in haskell, there might literally be more explanations of GADTs
       | on the internet than there are of typeclasses
        
         | progbits wrote:
         | > I believe there's a clippy lint for this, although I could be
         | mixing it up with the lint for Option::expect (which iirc tells
         | you to do unwrap_or_else in some cases)
         | 
         | It's one lint rule which covers bunch of these _or_else
         | functions: https://rust-lang.github.io/rust-
         | clippy/master/#or_fun_call
        
         | frizlab wrote:
         | Swift has guard. Equally awesome.
        
       | cibyr wrote:
       | Anyhow warrants more than an honorable mention, IMO.
       | anyhow::Context is great, and basically always an improvement
       | over unwrap() - whatever complaints you might have about
       | anyhow::Error, it's infinitely easier to handle than a panic.
        
       | Ar-Curunir wrote:
       | Really useful article to learn about idiomatic Rust :)
       | 
       | In general I think there is a lack of intermediate Rust material
       | that teaches you common design patterns, idiomatic Rust, and so
       | on.
       | 
       | Even I (someone who's written hundreds of thousands of fairly
       | complex Rust code) learnt about the let-else style solution from
       | this article =).
        
       | Zambyte wrote:
       | I have been using Zig a lot lately, and I just want to share the
       | equivalent of the let-else solution in Zig:
       | const user = getUser() orelse return error.NoUser;
       | 
       | If you only need user for a narrow scope like you would get from
       | match, you can also use if to unwrap the optional.
       | if (getUser()) |user| {             // use user         } else {
       | return error.NoUser;         }
        
         | robertlagrant wrote:
         | And the same in Python:                 if user := get_user()
         | is not None:         # use user       else:         # return
         | error
         | 
         | Although given the happy path code can mean you don't see the
         | error condition for ages, I much prefer this:
         | if (user := get_user()) is None:         # return error
         | # use user
        
           | aatd86 wrote:
           | hehehe. reminds me of _if err != nil_ in Go which is really
           | not an issue in my opinion. But it seems to have become
           | somewhat infamous in some circles.
        
             | jimbokun wrote:
             | The problem is the compiler doesn't help you if you forget
             | to check err.
             | 
             | Although it will flag unused variable. So you will have to
             | make an effort to deliberately ignore the error value.
             | 
             | Still not quite as nice as the compiler forcing you to
             | handle the error case.
        
               | int_19h wrote:
               | The problem is that idiomatic Go reuses err for multiple
               | calls. So if you already have one call and check err
               | after, it counts as used, and forgetting to check it on
               | subsequent calls is not flagged.
        
               | aatd86 wrote:
               | True. Not a big issue in practice and there are linters
               | and whatnot. Variable shadowing can happen.
               | 
               | But it's a bit orthogonal of a concern. I do have things
               | to say about errors but my complaints are a bit more
               | nuanced and made with hindsight.
        
             | LtWorf wrote:
             | It is a massive problem. It's basically the worse thing
             | about C and they decided to copy it.
             | 
             | Easily 60-70% of all go code is about propagating errors to
             | the caller directly.
        
               | aatd86 wrote:
               | Not C. C errors are different since they are simply
               | numbers.
               | 
               | And spoiler alert, every language propagates errors.
               | Sometimes automatically via exception handling, somewhat
               | simply by returning error values. rust does this too.
               | 
               | Matter fact, even javascript might be creeping toward
               | this model of explicit error propagation soon.
               | 
               | Good, because it is easier to understand.
        
           | fjasdfas wrote:
           | `:=` was new to me: https://peps.python.org/pep-0572/
           | 
           | It actually looks really natural in python, glad they added.
        
           | 38 wrote:
           | Python doesn't have option, so this is not the same thing at
           | all
        
         | fjasdfas wrote:
         | gleam:                   fn get_user_name() -> Result(String,
         | String) {             use user <-
         | result.map(option.to_result(get_user(), "No user"))
         | user         }
        
       | evrimoztamur wrote:
       | Talking about unwrapping: I've been using a rather aggressive
       | list of clippy lints to prevent myself from getting panics, which
       | are particularly deadly in real-time applications (like video
       | games). unwrap/expect_used already got me 90% of the way out, but
       | looking at the number of as conversions in my codebase, I think I
       | have around 300+ numerical conversions (which can and do fail!)
       | [lints.clippy]         all = "deny"         unwrap_used = "deny"
       | expect_used = "deny"         panic = "deny"
       | indexing_slicing = "deny"         unhandled_errors = "deny"
       | unreachable = "deny"         undocumented_unsafe_blocks = "deny"
       | unwrap_in_result = "deny"         ok_expect = "deny"
        
         | an_ko wrote:
         | Does that mean your code is annotated with 300+ instances of
         | `#[allow(clippy::unwrap_used)]` et al?
        
           | evrimoztamur wrote:
           | It was the first time I set it up, then I went through every
           | single instance and refactored with the appropriate choice.
           | It wasn't as tedious as you might imagine, and again, I
           | really don't have the option of letting my game crash.
           | 
           | I think the only legitimate uses are for direct indexing for
           | tile maps etc. where I do bounds checking on two axes and
           | know that it will map correctly. to the underlying memory
           | (but that's `clippy::indexing_slicing`, I have 0
           | `clippy::unwrap_used` in my codebase now).
           | 
           | If you begin a new project with these lints, you'll quickly
           | train to write idiomatic Option/Result handling code by
           | default.
        
         | p1necone wrote:
         | yoink (although I will probably allow expect - having to
         | provide a specific message means I'm only going to use it in
         | cases where there's some reasonable justification)
        
         | mplanchard wrote:
         | This is nice, but fairly miserable to deal with in in-module
         | unit tests, IMO.
         | 
         | We get around it by using conditional compilation and putting
         | the lints in our entrypoints (`main.rs` or `lib.rs`), which is
         | done automatically for any new entrypoint in the codebase via a
         | Make target and some awk magic.
         | 
         | As an example, the following forbids print and dbg statements
         | in release builds (all output should go through logging),
         | allows it with a warning in debug builds, and allows it
         | unconditionally in tests:
         | #![cfg_attr(not(debug_assertions), deny(clippy::dbg_macro))]
         | #![cfg_attr(not(debug_assertions), deny(clippy::print_stdout))]
         | #![cfg_attr(not(debug_assertions), deny(clippy::print_stderr))]
         | #![cfg_attr(debug_assertions, warn(clippy::dbg_macro))]
         | #![cfg_attr(debug_assertions, warn(clippy::print_stdout))]
         | #![cfg_attr(debug_assertions, warn(clippy::print_stderr))]
         | #![cfg_attr(test, allow(clippy::dbg_macro))]
         | #![cfg_attr(test, allow(clippy::print_stdout))]
         | #![cfg_attr(test, allow(clippy::print_stderr))]
         | 
         | AFAIK there isn't currently a way to configure per-profile
         | lints in the top-level Cargo configs. I wish there were.
        
           | indiv0 wrote:
           | We just set all the lints to `warn` by default then
           | `RUSTFLAGS="--deny warnings"` when building for release (or
           | in CI).
        
       | sophacles wrote:
       | > My main gripe with this error message is that it doesn't
       | explain why the ? operator doesn't work with Option in that
       | case... just that it doesn't.
       | 
       | The error in question:
       | 
       | > the `?` operator can only be used on `Result`s, not `Option`s,
       | in a function that returns `Result`
       | 
       | It literally tells you why it doesn't work, wtf do you want?
        
         | eviks wrote:
         | Indeed, and it even suggests the 2nd solution. All you have to
         | do is read from the top, not the bottom, of the error message
        
       | the__alchemist wrote:
       | Something I use for situations where nesting is getting out of
       | hand. Probably not idiomatic but, I find it practical in these
       | cases.                 if param.is_none() {         // Handle,
       | and continue, return an error etc       }       let value =
       | param.as_ref().unwrap(); // or as_mut       // Use `value` as
       | normal.
        
         | acjohnson55 wrote:
         | Any advantages of this over let-else?
        
           | the__alchemist wrote:
           | I wasn't familiar with let-else...
        
         | rav wrote:
         | You can rewrite it using let-or-else to get rid of the unwrap,
         | which some would find to be more idiomatic.
         | let value = Some(param.as_ref()) else {           // Handle,
         | and continue, return an error etc         }         // Use
         | `value` as normal.
        
           | the__alchemist wrote:
           | That part intrigued me about the article: I hadn't heard of
           | that syntax! Will try.
        
           | epidemian wrote:
           | Small nit: the Some() pattern should go on the left side of
           | the assignment:                   let Some(value) =
           | param.as_ref() else {           // Handle, and continue,
           | return an error etc         }         // Use `value` as
           | normal.
        
       | vmg12 wrote:
       | Also, sometimes just unwrap it. There is some software where it's
       | perfectly fine to panic. If there is no sane default value and
       | there is nothing you can do to recover from the error, just
       | unwrap.
       | 
       | Also, sometimes you just write software where you know the
       | invariant is enforced so a type is never None, you can unwrap
       | there too.
       | 
       | I find it interesting how a lot of people find Rust annoying
       | because idiomatic Rust is a very strict language. You still get a
       | ton of the benefits of Rust when writing non-idiomatic Rust. Just
       | use the Rc<RefCell<>> and Arc<Mutex> and feel free to unwrap
       | everything, nobody will punish you.
        
         | 0cf8612b2e1e wrote:
         | Plus it gives others the opportunity to post that xkcd
         | velociraptor strip.
        
           | iyn wrote:
           | https://xkcd.com/87/ this one?
        
           | Hackbraten wrote:
           | Do you mean this one? https://xkcd.com/292/
        
             | 0cf8612b2e1e wrote:
             | Yes, exactly. It is the Rust equivalent of goto.
        
               | vmg12 wrote:
               | Goto is bad because it results in very difficult to
               | reason about code. Using unwrap and expect is as bad as
               | using any other language without null safety.
        
               | int_19h wrote:
               | goto is bad _when_ it 's used in a way that makes it
               | difficult to reason about code, but not all uses of goto
               | are like that. The usual C pattern of `if (err) goto
               | cleanup_resources_and_return_err;` is a good example of
               | the use of goto that is not difficult to reason about.
               | 
               | Using unwrap/expect is still much better than using a
               | language without null safety because unwrap/expect make
               | it immediately obvious at which point a panic can occur,
               | and creates some friction for the dev writing the code
               | that makes them less likely to use it literally
               | everywhere.
        
         | p1necone wrote:
         | Imo always expect rather than unwrap in those cases. If it's
         | justifiable, you should justify it in the message.
         | 
         | ("This should never happen because: ..., if you see this
         | message there's a bug.")
        
           | Arnavion wrote:
           | Or let-else with a panic!() in the else {} if you want to use
           | a format string.
        
       | tengbretson wrote:
       | I'm not very familiar with Rust. Do the built in Option and
       | Result types not implement map and flatMap?
        
         | trealira wrote:
         | They do implement both of those, except instead of flat_map,
         | it's called and_then for Option and Result.
        
         | theon144 wrote:
         | They do, `map` and `and_then`.
         | 
         | As for the article, I'm also a bit confused because I'm really
         | not sure whether people write that sort of code at the
         | beginning "very commonly" - match and `ok_or` to handle None by
         | turning them into proper Errors is one of the first things you
         | learn in Rust.
        
         | Sharlin wrote:
         | As others have said, you can `and_then` chain `Options`, but
         | often it's better to convert each `Option` into a `Result`s
         | before chaining, to get more fine-grained error messages as
         | shown in the fine article. But usually it's cleaner and more
         | convenient (and friendlier to people used to exceptions) to use
         | the `?` operator which is basically Rust's `do` notation except
         | that _currently_ you can only early-return from the entire
         | function with it, not escape a specific block. Which in turn
         | requires the types to match, though Rust does at least insert
         | an `.into()` conversion for the error value.
        
       | rienbdj wrote:
       | How long till the Rust community starts to push do notation and
       | monad transformers?
        
         | keybored wrote:
         | Eight years ago?
        
       | Sharlin wrote:
       | > I find the name ok_or unintuitive and needed to look it up many
       | times. That's because Ok is commonly associated with the Result
       | type, not Option.
       | 
       | Hmm, I kind of disagree. The method literally returns "OK or an
       | error". It converts an Option into a Result and the name reflects
       | that.
       | 
       | There _is_ something of an inconsistency though, although IMHO
       | it's worth it. The `Result::ok()` method returns a Some if it's
       | Ok, and None otherwise, which is concise and intuitive but indeed
       | different from `Option::ok_or`.
        
       | tekacs wrote:
       | I use a setup like this:
       | 
       | https://gist.github.com/tekacs/60b10000d314f9923d6b6a5af8c35...
       | 
       | where... in my code, I have:                 some_block({ ...
       | }).infallible()
       | 
       | for cases where we believe that the Result truly should never
       | fail (for example a transaction block that passes through the
       | inner Result value and there is no Result value in the block) and
       | if it does then we've drastically misunderstood things.
       | 
       | Then, there's an enum (at the bottom of the file) of different
       | reasons that we believe that this should never fail, like:
       | // e.g. we're in a service that writes to disk... and we can't
       | write to disk
       | some_operation.invariant(Reason::ExternalIssue)       // we're
       | not broken, the system wasn't set up correctly, e.g. a missing
       | env var       some_operation.invariant(Reason::DevOps)       //
       | this lock was poisoned... there's nothing useful that we can do
       | _here_       some_operation.invariant(Reason::Lock)       //
       | something in this function already checked this
       | some_operation.invariant(Reason::ControlFlow)       // u64
       | overflow of something that we increment once a second... which
       | millennium are we in?
       | some_operation.invariant(Reason::SuperRare)
       | 
       | ... etc. (there are more Reason values in the gist)
       | 
       | This is all made available on both Result and Option.
        
       | mcflubbins wrote:
       | I with there was a clippy check for unwrap(), I have very very
       | rarely needed it in practice.
        
       ___________________________________________________________________
       (page generated 2025-05-13 23:01 UTC)