[HN Gopher] Beginner's guide to error handling in Rust
       ___________________________________________________________________
        
       Beginner's guide to error handling in Rust
        
       Author : rkwz
       Score  : 109 points
       Date   : 2022-07-10 15:39 UTC (7 hours ago)
        
 (HTM) web link (www.sheshbabu.com)
 (TXT) w3m dump (www.sheshbabu.com)
        
       | OJFord wrote:
       | Why isn't `Result<T, E>` just `Result<T, E> where E: std::Error`?
       | 
       | I love rust, but I hate 'no from<lib::Error> implementation for
       | my::Error' et al. 99.99% of the time I just want to bubble up a
       | string to print from anything I'm using that might error.
       | 
       | I've used anyhow and friends, it just still requires more from me
       | than I think should necessary.
       | 
       | But this is an honest question, several people put more thought
       | into it than I have and decided it should be so, why?
        
         | kccqzy wrote:
         | There are plenty of cases where the Err alternatives of Result
         | doesn't actually represent an error, just a case to be handled
         | differently. For example the binary search function
         | (https://doc.rust-
         | lang.org/std/primitive.slice.html#method.bi...) returns an
         | index of the insertion position when the search fails. That's
         | not usually an error to be surfaced.
         | 
         | I find that this distinction is basically the application vs
         | library distinction in the article. Application developers tend
         | to "just want to bubble up a string" whereas library developers
         | tend not to.
        
           | woodruffw wrote:
           | This is a good reason (for the purposes of stability), but
           | IMO a misuse of `Result` in the standard library. It really
           | ought to be something custom, closer perhaps to
           | `ControlFlow`[1] (which is newer). But even that's not a
           | great fit.
           | 
           | [1]: https://doc.rust-lang.org/std/ops/enum.ControlFlow.html
        
         | steveklabnik wrote:
         | One good reason to not do this is it's std::error::Error, but
         | there is no corresponding core::error::Error. no_std needs to
         | be able to use Result too.
        
           | OJFord wrote:
           | That feels a bit incomplete to me - that explains the
           | language mechanism existing for using something different,
           | but not std's lack of something more specific?
           | 
           | As in, std::result could use std::error::Error to specialise
           | core::result's more generic Result?
           | 
           | And it's barely any extra work at all for a maintainer of a
           | crate with a `no_std` feature, since you can still use the
           | same Result; it's no longer _required_ that its Error type
           | implements std 's, but it _can_?
        
             | steveklabnik wrote:
             | Rust doesn't yet have specialization. It is used internally
             | by the standard library, but only for optimizations, as far
             | as I know.
        
           | [deleted]
        
       | PointyFluff wrote:
       | ?;
        
       | sbt wrote:
       | The problem with errors in Rust is that the stdlib defined Error
       | as a trait and traits are hard to use (not sized, etc). The
       | current state of affairs with thiserror and anyhow is not
       | satisfactory if we want to ease the Rust learning curve.
       | 
       | Errors are too fundamental to be this hard.
        
       | Someone wrote:
       | FTA: let result = reqwest::blocking::get(url);
       | let response = match result {         Ok(res) => res,
       | Err(err) => return Err(err),       };
       | 
       | Is that truly idiomatic? I would think the _Err(err) = > return
       | Err(err)_ line needlessly constructs a copy of _result_. Or is
       | that necessary because of the borrow checker?
       | 
       | (Also, for those unfamiliar with it: _reqwest_ is not a typo. See
       | https://github.com/seanmonstar/reqwest)
        
         | throwaway234232 wrote:
         | > I would think the Err(err) => return Err(err) line needlessly
         | constructs a copy of result
         | 
         | It sounds like this is coming from a C++ bias? So please
         | forgive me if this is wrong.
         | 
         | Rust, in my experience, favors move semantics first, then copy
         | semantics after.
         | 
         | I know in C++, we had implicit copy constructors, with move
         | semantics after with rvalue references, where you need to use
         | `std::move` in a lot of cases.
         | 
         | So what helps, in my opinion, is to think of Rust as using
         | `std::move` as a default.
        
           | saghm wrote:
           | > So what helps, in my opinion, is to think of Rust as using
           | `std::move` as a default
           | 
           | Even more than that, with the exception of types that
           | implement `Copy` (e.g. bools, integers, etc.), using after a
           | move won't just silently degrade to a copy, but will cause a
           | compiler error. Copying is required to be explicit for all
           | but the most trivial types.
        
         | richardwhiuk wrote:
         | The idiomatic way to write this would be:
         | 
         | let response = reqwest::blocking::get(url)?;
         | 
         | In the way written there's no additional copy.
        
         | masklinn wrote:
         | > Or is that necessary because of the borrow checker?
         | 
         | It's not because of the borrow checker, it's because Rust
         | defaults to (destructive) moves, and implicit copies can only
         | be trivial.
         | 
         | Any non-trivial copy has to go through an explicit clone()
         | instead.
        
         | jcranmer wrote:
         | > I would think the Err(err) => return Err(err) line needlessly
         | constructs a copy of result.
         | 
         | You would think wrong. In Rust, everything is move by default.
         | To create a copy, you need to explicitly call .clone() on an
         | object (assuming it implements Clone, of course), or you can
         | implement Copy for the type, which will cause .clone() to be
         | called instead of moving the value.
         | 
         | (I'm not actually sure where the compiler will generate calls
         | to clone() on Copy types in favor of moving. In practice,
         | you're not going to implement Copy for a type that isn't
         | trivially copyable anyways [using the C++ definition], so any
         | optimizer would easily be able to elide any excessive copy
         | operations.)
        
           | joaonmatos wrote:
           | Copy types must be trivially copiable bitwise (the clone
           | implementation is returning the deref of the cloned type), so
           | a copy is a move except for the fact that the previous
           | reference is not invalidated.
           | 
           | The docs for the trait (core::marker::Copy) are pretty good
        
         | Ambroisie wrote:
         | As pointed out, the idiomatic way to do this is to use the try
         | operator (`?`).
         | 
         | But to answer your question about copies : Rust is a move-only
         | language, copies are actually called `.clone()`, except for a
         | few types which are cheap enough to copy that they implement
         | the `Copy` trait.
         | 
         | So in the code you quoted, the match is done on the value of
         | `result` (notice that there is not borrowing/`&` operator). The
         | match arm `Err(err)` moves `err` out of `result` and returns
         | it. Obviously, the compiler will optimize away all those moves,
         | it's as if they did not exist.
        
           | littlestymaar wrote:
           | > But to answer your question about copies : Rust is a move-
           | only language, copies are actually called `.clone()`, except
           | for a few types which are cheap enough to copy that they
           | implement the `Copy` trait.
           | 
           | This is almost true, but not exact. Copy is for everything
           | where cloning is just _memcopy_ , that doesn't means it's
           | necessarily cheap ( _[42;4_000_000]_ implements _Copy_ , yet
           | it's not cheap to copy at all...).
           | 
           | And moving things sometimes (but not always) means the thing
           | is getting memcopied (or it could use a pointer, depending on
           | the optimizer's mood euristics)
        
       | iratewizard wrote:
        
         | mlindner wrote:
         | How is someone who submitted links only three times since
         | September last year a spammer?
         | 
         | Yes they're self-promoting their own blog, but I don't see the
         | problem wit that.
        
           | iratewizard wrote:
           | > Please don't use HN primarily for promotion. It's ok to
           | post your own stuff occasionally, but the primary use of the
           | site should be for curiosity
           | 
           | 90% of their posts are self promotion and they've done it
           | more than 50 times.
        
           | nindalf wrote:
           | That's not quite it. They definitely had a time in 2020 where
           | they would repeatedly submit links to their website over and
           | over. I can see why they'd do that, because getting to the
           | front page involves luck. But still, not cool.
        
             | littlestymaar wrote:
             | Unless they've tampered with their submissions history[1],
             | I only see at most two submissions for the same blog post,
             | one day apart. Would I be OK with someone posting the same
             | link over and over half a dozen time, no, but a single
             | retry sounds fair to me.
             | 
             | [1]: https://news.ycombinator.com/submitted?id=rkwz
        
       | c7DJTLrn wrote:
       | I found error handling in Rust to be an unergonomic nightmare and
       | is one of the reasons I don't want to write it anymore. Usually
       | people recommend crates designed to make the experience better
       | but that's a failure in my opinion. Every project ends up doing
       | it differently -- anything like that should be in the standard
       | library. In Go, you can build massive projects without needing
       | any dependencies to make error handling ergonomic.
        
         | ParetoOptimal wrote:
         | > In Go, you can build massive projects without needing any
         | dependencies to make error handling ergonomic.
         | 
         | Because it's always unergonomic?
        
         | nindalf wrote:
         | I was with you until the end. Go errors leave something to be
         | desired - it's not easy to wrap, compare errors or print stack
         | trace. These can be fixed with the right library and
         | conventions, but the default error handling is only good enough
         | for small and medium projects. (In my subjective opinion).
         | 
         | Agree that Rust errors need to be better. anyhow and thiserror
         | are fine libraries but I want that experience out of the box,
         | ideally. There are ongoing efforts to improve things, so I'm
         | hopeful.
        
           | c7DJTLrn wrote:
           | >it's not easy to wrap                 fmt.Errorf("... %w",
           | err)
           | 
           | >compare errors                 errors.Is(err, ErrorType)
        
             | [deleted]
        
             | IshKebab wrote:
             | If you're happy with `Errorf()` in Go then you can just use
             | `anyhow` in Rust. Same trade-off. Easy type-erased error
             | handling.
             | 
             | I would recommend it for application code. For library code
             | you should go the extra mile and use proper error types.
             | 
             | I think that Go's error handling is actually fine and most
             | people are making a fuss about nothing, but even so Rust's
             | error handling is clearly superior.
        
             | nemothekid wrote:
             | Except when you do (1), your "wrapped" error can longer be
             | compared. You end up with the same Rust-style boiler plate
             | of building your own error structs or you eschew the
             | ability to inspect your error types at all.
             | 
             | For example, if I have some library that makes a network
             | request and that request times out do I:
             | 
             | 1. Return net.Error?
             | 
             | 2. fmt.Errorf?
             | 
             | 3. My own custom type
             | 
             | The problem with (1), is that API is invisible; the caller
             | is now expected to trawl through my code or documentation
             | for any errors I might return that the caller might want to
             | handle. (2) prevents the caller for acting on my error
             | without string mangling (and again has to inspect my code
             | for the exact strings to handle. (3) is the best, but has
             | the same level of boilerplate; and is also, I've found,
             | unidiomatic in Go.
             | 
             | The second blight of Go's error handling situation (after
             | if err == nil), is errors are rarely part of an APIs
             | contract; especially outside of the standard library. Often
             | times if you want to handle a special error code for a
             | given API it means actually opening up the source code and
             | hunting down where that error is thrown and hoping that (1)
             | the error type is public or (2) that the library author
             | won't break your string mangling in a later version.
        
               | c7DJTLrn wrote:
               | >Except when you do (1), your "wrapped" error can longer
               | be compared
               | 
               | But that's wrong. When you call String() on it, it will
               | look like the error is just a concatenated string, but it
               | preserves all information about the underlying error,
               | that's the whole point of the %w directive.
               | 
               | https://go.dev/play/p/ZUGCKJDs9N5
        
               | nemothekid wrote:
               | No; I don't mean the underlying error. I mean the wrapped
               | error.
               | 
               | Let's say I'm writing an API client that uses net/http
               | underneath -                   func CreateUser(username
               | string, password string) error {              _, err :=
               | client.Get("https://myapi.com");              return
               | fmt.Errorf("Failed to create: %w", err)         }
               | 
               | Then I create another method like so:
               | func SSOLogin(provider interface{}, token string) error {
               | user, pass, err = provider.login();              if err
               | != nil {                  return fmt.Errorf("Failed to
               | login: %w", err)              }              if err :=
               | CreateUser(user, pass); err != nil {
               | return fmt.Errorf("Failed to create: %w", err)
               | }         }
               | 
               | There is no way for the API consumer to discover if the
               | error happened at provider.login() or CreateUser (except
               | for calling String, and Finding "Failed to create". The
               | wrapping is one layer deep, and to go any further you
               | must create your own types. This commonly happens in
               | larger codebases; and what happens is you must define
               | your own error types if you expect your code to be
               | consumed by others.
               | 
               | To be clear, there are way to solve this (creating your
               | own types), but I don't think go handles that better than
               | Rust, and the tools that Go does give you push developers
               | in wrapping/concatenating errors into rather opaque types
               | (which is mostly fine for webservers, you can just throw
               | an error 500 and expect the user to try again). But that,
               | plus `if err == nil` makes error handling in Go rather
               | sore IMO. It's easy for developers to write, but not for
               | library users to use, or for later users to maintain.
        
             | the_gipsy wrote:
             | errors.Is only works if you have an actual ErrorType which
             | you have to manually hunt down only to find it's a
             | fmt.Errof and it doesn't work.
        
               | c7DJTLrn wrote:
               | It's rarely the case that you need to know exactly where
               | the error originated. You just need to check if the error
               | wraps a network error or EOF error for example.
               | 
               | If you do need to know specifically what the error was
               | then the API provides public error types.
        
               | the_gipsy wrote:
               | No, "the API" doesn't. I have encountered many libs that
               | just do fmt.Errof, and just like that application code
               | does too.
               | 
               | So go doesn't do error handling well at all. Give your
               | stdlib a stringly error maker, that's what will creep in
               | everywhere. It doesn't matter if there is a section for
               | creating (somewhat) typed errors - half of go code
               | doesn't bother and together with the poor type system you
               | can't even be sure which type of error you got.
        
               | nindalf wrote:
               | I wouldn't bother replying further. Clearly they've got a
               | very strong opinion on this. Multiple people have pointed
               | out the issues with their line of thought but they
               | continue to persist. They'll understand if they reflect
               | on it, maybe.
        
               | the_gipsy wrote:
               | Yea. Actually, they should get hands on with error
               | handling other than go or your run of the mill
               | exceptions.
        
         | jcelerier wrote:
         | > In Go, you can build massive projects without needing any
         | dependencies to make error handling ergonomic.
         | 
         | are we talking about the same go where the average code looks
         | like this, a big long chain of if blabla return nil, err ?
         | 
         | https://github.com/cockroachdb/cockroach/blob/5fb4478b94ecaf...
         | 
         | like, code like this is exactly the reason why exceptions were
         | invented
        
         | lijogdfljk wrote:
         | If the bar was as low as Go it would be quite easy in Rust.
         | It's just string concat after all.. but i'm quite glad they
         | didn't use that low bar, it's awful to deal with imo.
        
           | c7DJTLrn wrote:
           | >It's just string concat after all
           | 
           | If you're properly wrapping errors it isn't.
           | fmt.Errorf("... %w", err)
        
             | lijogdfljk wrote:
             | Has the meaning of that changed since i used Go? I thought
             | the output of that is _literally_ a string, no?
             | 
             | I think you misunderstand me. I'm not complaining that
             | there is no context. I'm complaining about destroying all
             | structured content. If you want information out of that,
             | you have to string parse your errors! No type checking or
             | anything useful on error types either.
             | 
             | Yes, it's possible, but that usually comes with 3rd party
             | libraries that manage errors with structure. Such as a
             | large array of arrays producing a structured "stack". But
             | if a single person uses string formatting with your nice
             | structured error? Goodbye structure, hello string parsing.
             | 
             | It's a very, very low bar.
        
               | c7DJTLrn wrote:
               | >I thought the output of that is _literally_ a string, no
               | 
               | >If you want information out of that, you have to string
               | parse your errors
               | 
               | I'm trying not to be an asshole here, but have you
               | actually played with Go? It seems a lot of the Go hate
               | comes from people with extremely trivial knowledge of it.
               | 
               | The %w directive is not just a simple string
               | format/concat. It constructs an error which when String()
               | is called on it, will _appear_ as a concat 'd string, but
               | the underlying error is preserved.
               | 
               | https://go.dev/play/p/ZUGCKJDs9N5
        
               | lijogdfljk wrote:
               | Yea, professionally for 5 years, though it's been a good
               | 4 years since i used it.
               | 
               | Looking at the release notes, %w was introduced in 2019.
               | So yea, my professional experience with Go predated that.
               | It was all strings back then, and miserable. My team
               | usually used libraries to deal with the shortcomings.
               | 
               | I ended up leaving Go frustrated mostly because of how
               | simple patterns, like Iterators (in Rust) were so
               | horribly expressed in Go. Stretched out into many, many
               | lines of code. All the simplicity of the language left me
               | with no help to manage my actual app complexity.
               | 
               | However i am aware that slowly Go is adding complexity to
               | help you manage real problems. Generics, and apparently
               | sane error handling. I'd love to see them add Enums, as
               | Go's version always felt ... well, just a bunch of consts
               | in userland pretending to be Enums heh.
               | 
               | Maybe one day Go will be feature complete enough for me
               | to look back. Though i'm a big fan of Rust, so maybe not.
               | The only area i feel Go is better than Rust is the way in
               | implements Async, but that's likely more to do with the
               | GC - so, tradeoffs as with everything.
        
         | throwaway234232 wrote:
         | > Usually people recommend crates designed to make the
         | experience better but that's a failure in my opinion.
         | 
         | I agree, I love Rust, but I'd like to see error handling
         | ergonomics improved.
         | 
         | I'd like to see error handling become more like Zig or Swift.
         | 
         | Until then, I cope with the `anyhow` and `thiserror` crate :|
        
           | ReactiveJelly wrote:
           | I guess I have the Blub effect. Coming from C++ and C#, Rust
           | has my favorite error handling so far.
        
       | bluejekyll wrote:
       | This is a good intro, while it mentions `thiserror`, I personally
       | can't recommend this enough. For anyone building a library, the
       | ability to wrap underlying errors and generate a From
       | implementation quickly for them (converts from source error to
       | target) is it's super power. It also takes a simple error string
       | to create the Display implementation. It does all of this and
       | generates code and types that are consistent with the std
       | library.
       | 
       | It's good to understand the error mechanics, but don't wast time
       | writing custom errors, just use `thiserror`.
        
         | lytedev wrote:
         | Seconded! It also helps in my code that I can see all the
         | things that can go wrong for a given module. Sure, this isn't
         | unique to "thiserror" because you can do this with any enum,
         | but it's much cleaner while being sensible and explicit.
        
         | eptcyka wrote:
         | For years, there have been definitive error crates, each with
         | it's own monstrocity of macro to generate more code, and for
         | every new one there have been painful migrations for codebases
         | of any significant size. Writing out the error types manually
         | isn't actually that difficult.
        
           | littlestymaar wrote:
           | > and for every new one there have been painful migrations
           | for codebases of any significant size
           | 
           | Why did you migrate though? At work, we're still using
           | error_chain for the oldest library we have since the
           | ergonomics of the library isn't an issue for these stable-ish
           | libs. If what you already have works, just don't migrate.
           | It's not like there's likely hidden segfaults or memory vulns
           | waiting there...
        
             | kelnos wrote:
             | > _It 's not like there's likely hidden segfaults or memory
             | vulns waiting there..._
             | 
             | It's funny you mention that, because the now-deprecated
             | 'failure' crate has such a memory-safety vulnerability:
             | https://github.com/rust-lang-deprecated/failure/issues/336
             | 
             | Granted, consuming code is only vulnerable if they opt-in
             | to implementing a provided trait method that most people
             | should never ever need to implement. But I would still try
             | to eliminate 'failure' from my dependency graph if possible
             | (and I recently submitted PRs to two dependencies I was
             | using to remove 'failure' from _their_ dependencies).
        
           | tempest_ wrote:
           | For library code thiserror has been by far the best
           | experience, sure I can write out the error code manually but
           | why would I waste my time pumping out boiler plate.
           | 
           | The combination of thiserror and anyhow have been my go to
           | for nearly 3 years without issue.
        
           | stormbrew wrote:
           | thiserror is quite a lot lighter than most of the prior ones
           | have been. It also has less magic than they tended to, which
           | is nice. It just does what it says it's gonna do mostly.
           | 
           | Honestly I think thiserror and anyhow strike the right
           | balances that I think they could be candidates for stdlib
           | inclusion, and then might be more optimized.
        
         | burntsushi wrote:
         | If you're writing internal libraries or libraries that are
         | otherwise already using proc macros, sure, but I don't
         | otherwise agree with this advice personally. It takes me maybe
         | a couple of extra minutes to write out the error type and
         | associated code. In exchange, every single compilation of my
         | library will be faster as a result.
         | 
         | To be clear, 'thiserror' is great and it would probably fit
         | well inside of std. Then that extra compilation time I'm
         | complaining about could be avoided.
        
           | guitarbill wrote:
           | I don't disagree with this... but is there anything wrong
           | with recommending `thiserror` by default, especially for less
           | experienced Rustaceans? It's fairly sane, encourages "good"
           | error design. I wish there'd been something like it when I
           | started.
           | 
           | And moving forwards, you could a) still write out the error
           | type and associated code to replace `thiserror` fairly
           | easily, or b) possibly benefit from any work being done to
           | speed up proc macros in the future. So I feel like
           | recommending it as a default is still the best option?
        
             | burntsushi wrote:
             | That's a harder question to answer IMO with less clear
             | trade offs. It gets into pedagogical style. I think for me,
             | it depends on what's being done, but I can see how it might
             | be a decent recommendation by default. I think you can
             | afford a sentence about its cost though, so I'd suggest
             | including that. When in doubt, present the trade off.
        
         | stevefan1999 wrote:
         | The only problem with thiserror is the lack of no_std tho,
         | which leads me to use snafu instead
        
       ___________________________________________________________________
       (page generated 2022-07-10 23:01 UTC)