Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Beginner's guide to error handling in Rust (sheshbabu.com)
150 points by rkwz on July 10, 2022 | hide | past | favorite | 57 comments


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


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.


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?


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.


It might be worth doing this on a stable crate, that doesn’t have a lot of dependencies.

For me, when there are so many it’s a trade-off. Sure, hand coding it isn’t hard, maybe you can even build a macro_rules that can deal with most of the down the middle cases, but ultimately thiserror makes development faster and simpler.

It’s always something that can be optimized away pretty easily later.


Yeah I guess I've just never been in a situation where making the definition of error type take ~1 minute vs ~5 minutes (for example) was a difference maker. Firstly because the frequency of defining new error types is so low. And secondly because it often takes me longer to even figure out what the error type should look like. The savings from 'thiserror' is basically just in the noise for me.

I would use it if it were in 'std' though. I would also use it in a project that already had a lot of dependencies, since it's likely such a project is probably already bringing in the proc-macro stuff.


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.


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.


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.


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


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


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.


The only problem with thiserror is the lack of no_std tho, which leads me to use snafu instead


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.


> 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


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.


>it’s not easy to wrap

  fmt.Errorf("... %w", err)
>compare errors

  errors.Is(err, ErrorType)


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.


>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


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.


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.


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.


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.


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.


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.


Yea. Actually, they should get hands on with error handling other than go or your run of the mill exceptions.


> In Go, you can build massive projects without needing any dependencies to make error handling ergonomic.

Because it's always unergonomic?


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.


>It's just string concat after all

If you're properly wrapping errors it isn't.

  fmt.Errorf("... %w", err)


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.


>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


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.


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


I guess I have the Blub effect. Coming from C++ and C#, Rust has my favorite error handling so far.


I’m sorry, I honestly can’t take your take seriously if you believe Go’s error handling is remotely ergonomic.


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?


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.


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


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.


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?


Rust doesn't yet have specialization. It is used internally by the standard library, but only for optimizations, as far as I know.


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.


I’m not sure there is a way out - this is the fundamental aspect of low level languages. If you want to care about memory allocation, you have to care about it everywhere.

Rust is a really great language, but I believe it is a trap to go down this “high-level low-level language” route C++ also chose.


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)


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.


> 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 m̶o̶o̶d̶ euristics)


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


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


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


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


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


The idiomatic way to write this would be:

let response = reqwest::blocking::get(url)?;

In the way written there's no additional copy.


?;


[flagged]


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.


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.


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


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




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

Search: