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