It's hard. Python 2.x had a good error exception hierarchy, which made it possible to sort out transient errors (network, remote HTTP, etc.) errors from errors not worth retrying. Python 3 refactored the error hierarchy, and it got worse from the recovery perspective, but better from a taxonomy perspective.
Rust probably should have had a set of standard error traits one could specialize, but Rust is not a good language for what's really an object hierarchy.
Error handling came late to Rust. It was years before "?" and "anyhow". "Result" was a really good idea, but "Error" doesn't do enough.
This is the kind of stuff I would rather not have outsourced for 3rd party dependencies.
Every Rust project starts by looking into 3rd party libraries for error handling and async runtimes.
I'm a recent snafu (https://docs.rs/snafu/latest/snafu/) convert over thiserror (https://docs.rs/thiserror/latest/thiserror/). You pay the cost of adding `context` calls at error sites but it leads to great error propagation and enables multiple error variants that reference the same source error type which I always had issues with in `thiserror`.
No dogma. If you want an error per module that seems like a good way to start, but for complex cases where you want to break an error down more, we'll often have an error type per function/struct/trait.
>This means, that a function will return an error enum, containing error variants that the function cannot even produce. If you match on this error enum, you will have to manually distinguish which of those variants are not applicable in your current scope
You have to anyway.
The return type isn't to define what error variants the function can return. We already have something for that, it's called the function body. If we only wanted to specify the variants that could be returned, we wouldn't need to specify anything at all: the compiler could work it out.
No. The point of the function signature is the interface for the calling function. If that function sees an error type with foo and bar and baz variants, it should have code paths for all of them.
It's not right to say that the function cannot produce them, only that it doesn't currently produce them.
Lately, I've been using io::Error for so many things. (When I'm on std). It feels like everything on my project that has an error that I could semantically justify as I/O. Usually it's ErrorKind::InvalidData, even more specifically. Maybe due to doing a lot of file and wire protocol/USB-serial work?
On no_std, I've been doing something like the author describes: Single enum error type; keeps things simple, without losing specificity, due the variants.
When I need to parse a utf-8 error or something, I use .map_err(|_| ...)
After reading the other comments in this thread, it sounds like I'm the target audience for `anyhow`, and I should use that instead.
Kind of reminds me of Java checked exceptions.
I find TS philosophy of requiring input types and inferring return types (something I was initially quite sceptical about when Flow was adopting it) quite nice to work with in practice - the same could be applied to strict typing of errors ala Effect.js?
This does add the “complexity” of there being places (crate boundaries in Rust) where you want types explicitly defined (so to infer types in one crate doesn’t require typechecking all its dependencies). TS can generate these types, and really ought to be able to check invariants on them like “no implicit any”.
Rust of course has difference constraints and hails more from Haskell’s heritage where the declared return types can impact runtime behavior instead. I find this makes Rust code harder to read unfortunately, and would avoid it if I could in Rust (it’s hard given the ecosystem and stdlib).
As a bit of an aside, I get pretty far just rolling errors by hand. Variants fall into two categories, wrappers of an underlying error type, or leafs which are unique to my application.
For example,
enum ConfigError {
Io(io::Error),
Parse { line: usize, col: usize },
...
}You could argue it would be better to have a ParserError type and wrap that, and I absolutely might do that too, but they are roughly the same and that's the point. Move the abstraction into their appropriate module as the complexity requests it.
Pretty much any error crate just makes this easier and helps implement quality `Display` and other standard traits for these types.
> And so everyone and their mother is building big error types. Well, not Everyone. A small handful of indomitable nerds still holds out against the standard.
The author is a fan of Asterix I see :)
> The current standard for error handling, when writing a crate, is to define one error enum per module…
Excuse me what?
> This means, that a function will return an error enum, containing error variants that the function cannot even produce.
The same problem happens with exceptions.
The error library he seems looking for is „error_mancer“
Yeah… Please no.
I’m getting a bit of a macro fatigue in Rust. In my humble opinion the less “magic” you use in the codebase, the better. Error enums are fine. You can make them as fine-grained as makes sense in your codebase, and they end up representing a kind of an error tree. I much prefer this easy to grok way to what’s described in the article. I mean, there’s enough things to think about in the codebase, I don’t want to spend mental energy on thinking about a fancy way to represent errors.
I don’t really agree with this. The vast majority of the time, if you encounter an error at runtime, there’s not much you can do about it, but log it and try again. From there, it becomes about bubbling the error up until you have the context to do that. Having to handle bespoke error type from different libraries is actually infuriating, and people thinking this is a good idea makes anyhow mandatory for development in the language.
I quite like the Rust approach of Result and Option. The anyhow and thiserror crate are pretty good. But yeah I constantly get confused by when errors can and can not coerce. It's confusing and surprising and I still run into random situations I can't make heads or tails from.
I don't know what the solution is. And Rust is definitely a lot better than C++ or Go. But it also hasn't hit the secret sauce final solution imho.
Is it just me or is the margin/padding altered. I notice this article (being first) is squished up against the orange header bar
To skip recreating OOP in Rust, use anyhow instead.
Go got this right. Lua also has a nice error mechanism that I haven't seen elsewhere where you can explicitly state where in the call stack the error is occurring (did I error from the caller? or the callee?).
Similarly, JavaScript seems to do OK, but I miss error levels. And C seems to also have OK error conventions that aren't too bad. There's a handful of them, and they're pretty uncontroversial.
Macros seem to be wrong in every language they're used, because people can't help themselves.
It's like a red flag that the language designers were OK giving you enough rope to hang yourself with, but also actively encourage you to kill yourself because why else would you use the rope for anything else?
Rust should seriously stop using macros for everything.
This looks nice, especially for a mature/core library.
If your API already maps to orthogonal sets of errors, or if it's in active development/iteration, you might not get much value from this. But good & specific error types are great documentation for helping developers understand "what can go wrong," and the effects compound with layers of abstraction.
This article and comment section are making me feel like one of the only people that like error handling in Rust? I usually use an error for the crate or application with an enum of types. Maybe a more specific error if it makes sense. I don't even use anyhow or this error.
I like it better then python and go.
I apologize for this sidebar. I don’t have much to contribute to the technical content. It was an interesting read.
You use, too many, commas, in your, writing. It’s okay to have a long sentence with multiple phrases.
Thanks for sharing your thoughts.
I disagree that the status quo is “one error per module or per library”. I create one error type per function/action. I discovered this here on HN after an article I cannot find right now was posted.
This means that each function only cares about its own error, and how to generate it. And doesn’t require macros. Just thiserror.