Beginner's guide to error handling in Rust
sheshbabu.com
sheshbabu.com
It’s good to understand the error mechanics, but don’t wast time writing custom errors, just use `thiserror`.
The combination of thiserror and anyhow have been my go to for nearly 3 years without issue.
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.
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 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).
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.
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?
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.
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.
Because it's always unergonomic?
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.
fmt.Errorf("... %w", err)
>compare errors errors.Is(err, ErrorType)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.
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.
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 do need to know specifically what the error was then the API provides public error types.
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 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.
If you're properly wrapping errors it isn't.
fmt.Errorf("... %w", err)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.
>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.
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.
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 :|
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 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?
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?
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.
[1]: https://doc.rust-lang.org/std/ops/enum.ControlFlow.html
Errors are too fundamental to be this hard.
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.
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)
let response = reqwest::blocking::get(url)?;
In the way written there's no additional copy.
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.
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)
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.
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.
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.
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.)
The docs for the trait (core::marker::Copy) are pretty good