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