70 comments

[ 5.1 ms ] story [ 80.9 ms ] thread
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.

Agreed about magic.

Please correct me if I’m misunderstanding this, but something that surprised me about Rust was how there wasn’t a guaranteed “paper trail” for symbols found in a file. Like in TypeScript or Python, if I see “Foo” I should 100% expect to see “Foo” either defined or imported in that specific file. So I can always just “walk the paper trail” to understand where something comes from.

Or I think there was also a concept of a preamble import? Where just by importing it, built-ins and/or other things would gain additional associated functions or whatnot.

In general I just really don’t like the “magic” of things being within scope or added to other things in a manner that it’s not obvious.

(I’d love to learn that I’m just doing it wrong and none of this is actually how it works in Rust)

Yes. Macros are a hammer, but not everything is a nail.

Declarative macros (macro_rules) should be used to straightforwardly reduce repetitive, boilerplate code generation and making complex, messy things simpler.

Procedural macros (proc_macro) allow creating arbitrary, "unhygienic" code that declarative macros forbid and also custom derive macros and such.

But it all breaks down when use of a library depends too much on magic code generation that cannot be inspected. And now we're back to dynamic language (Ruby/Python/JS) land with opaque, tinkering-hostile codebases that have baked-in complexity and side-effects.

Use magic where appropriate, but not too much of it, is often the balance that's needed.

Both sides have been a pain for me. Either I’m debugging macro errors or else I’m writing boilerplate trait impls all day… It feels like a lose/lose. I have yet to find a programming language that does errors well. :/
Annotations were once condemned as 'magic' for doing things at runtime. Now it's apparently fine to use a language where most non-trivial code depends on macros. Tools that rewrite your code at compile time, often invisibly. But hey, it's not magic if it's your magic, right?
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.

Is it really though? What’s the point of having an error type per function? As the user of std::io, I don’t particularly care if file couldn’t be read in function foo, bar, or baz, I just care that the file couldn’t be read.
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.

That's the way, but I find it quite painful at time. Adding a new error variant to a function means I now have to travel up the hierarchy of its callers to handle it or add it to their error set as well.
That's only the case when your per-function error sets are "flat" (directly contain "leaf" errors from many layers below).

You can avoid this issue if you deeply nest the error types (wrap on every level). It you change an error that's not deeply-matched anywhere, you only need to update the direct callers. But "deep" errors have some tradeoffs [1] too

[1]: https://news.ycombinator.com/item?id=44420061

I thought I was a pedantic non-idiomatic weirdo for doing this. But it really felt like the right way---and also that the language should make this pattern much easier.
Have an example that I can read. I also use this error but struggle a bit when it comes to deciding how fine grained or like in the article, how big an error type should be.
I suspect you're referring to this article, which is a good read indeed: https://mmapped.blog/posts/12-rust-error-handling
Thanks for sharing!

The only thing I disagree with is error wrapping. While I agree that the main error should not expose dependencies, I find it useful to keep a `cause` field that corresponds to the inner error. It's important to trace the origin of an error and have more context about it. By the way, Rust's [Error] trait has a dedicated `cause` method for that!

[Error] https://doc.rust-lang.org/nightly/core/error/trait.Error.htm...

> I disagree that the status quo is “one error per module or per library”.

It is the most common approach, hence, status quo.

> I create one error type per function/action. I discovered this here on HN after an article I cannot find right now was posted.

I like your approach, and think it's a lot better (including for the reasons described in this article). Sadly, there's still very few of us taking this approach.

`thiserror` is a derive macro :)
> I create one error type per function/action

I do too! I've been debating whether I should update SNAFU's philosophy page [1] to mention this explicitly, and I think your comment is the one that put me over the edge for "yes" (along with a few child comments). Right now, it simply says "error types that are scoped to that module", but I no longer think that's strong enough.

[1]: https://docs.rs/snafu/latest/snafu/guide/philosophy/index.ht...

I accept, however this requires to create many types with corresponding implementations (`impl From`, `impl Display`, ...). This is a lot of boilerplate.
Why one error type per function. That seems overkill. Can you explain the need
See the two articles linked in the sibling comments
How does that compose? If you call somebody else's function, do you just create a superset of all possible errors they can return? What if it is a library that doesn't really specify what errors an individual function can return, but just has errors for the whole library?
You return an error specific to that function.

If it internally has a `InnerFuncErr::WriteFailed` error, you might handle it, and then you don't have to pass it back at all, or you might wrap it in an `OuterFuncErr::BadIo(inner_err)`or throw it away and make `BadIo` parameterless, if you feel that the caller won't care anyways.

Errors are not Exceptions, you don't fling them across half of your codebase until they crash the process, you try to diligently handle them, and do what makes sense.

So you don't really care about the union.

There's a bunch of different situations that can be discussed, and it's hard to generalise. However:

Your function typically has a specific intent when it tries to call another function. Say that you have a poorly designed function that reads from a file, parses the data, opens a DB connection and stores the data.

Should I really expect an end-user to understand an error generated by diesel/postgres/wtfdb? No, most likely I want to instruct them to generate debug logs and report an issue/contact support. This is most likely the best user experience for an application. In this case, each "action" of the function would "hide" the underlying error––it might provide information about what failed (file not found, DB rejected credentials, what part of the file couldn't be parsed, etc), but the user doesn't care (and shouldn't!) about Rust type of diesel error was generated.

To answer your question specifically, I might go without something like this:

    #[derive(Debug, Error, Clone)]
    pub enum MyFunctionError {
        #[error("unable to read data from file: {0}")]
        ReadData(String),

        #[error("failed to parse data: {0}")]
        Parse(String),

        #[error("database refused our connection: {0} (host: {1}, username: {2})")]
        DatabaseConnection(String, String, String),

        #[error("failed to write rows: {0}")]
        WriteData(String),
    }
Obviously this is a contrived example. I wouldn't use `#[from]` and just use `.map_err` to give internal meaning to error provenances. `DatabaseConnection` and `WriteData` might have come from the same underlying WTFDbError, but I can give it more meaning by annotating it.

When building a library, however, yes, I most likely do want to use `#[from] io::Error` syntax and let the calling library figure out what to do (which might very well giving the user a userful error message and dumping an error log).

Rust should seriously stop using macros for everything.
I agree with this. It appears that in this case, macros are a band-aid over missing features in the type system.

I feel like structural typing or anonymous sum types would solve this problem without macros.

I mean given A = X | Y and B = X | Y | Z, surely the compiler can tell that every A is a B?

What's wrong with macros?

Especially attribute style macros which apply to variants of an enum allow a lot of code expansion which reduces boilerplate a lot.

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.

`Result` is just `Either` by another name, and the main idea is to use sum and product types as the result of a computation instead of throwing, which turns error handling into business as usual. The `Result` type and `Error` trait really are orthogonal, and as soon as the `Try` trait is stabilized I think we'll see some good improvements.
There’s much not to like about C++ exceptions, I get it, but as a C++ programmer the proliferation of error types in Rust rubs me the wrong way. I like that C++ defines a hierarchy of exceptions, like Python, and you are free to reuse them. I do not want to go to the hassle of defining some new error types everywhere, I just want to use equivalents to runtime_error or logic_error. It feels like Rust is multiplying unnecessarily.
> I do not want to go to the hassle of defining some new error types everywhere, I just want to use equivalents to runtime_error or logic_error.

You can use anyhow::Error everywhere. If you need to "catch" a specific wrapped error, you can manually document it on the "throwing" method and downcast where you "catch". It's very similar to exceptions (checked-but-unspecific `throws Exception` in Java), but better, because Result and ? are explicit.

(comment deleted)
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.

Or rather every rust project starts with cargo install tokio thiserror anyhow.

If we just added what 95% of projects are using to the standard library then the async runtime would be tokio, and error handling would be thiserror for making error types and anyhow for error handling.

Your ability to go look for new 3rd party libraries, as well as this article's recommendations, are examples of how Rust's careful approach to standard library additions allows the ecosystem to innovate and try to come up with new and better solutions that might not be API compatible with the status quo

Is it just me or is the margin/padding altered. I notice this article (being first) is squished up against the orange header bar
> 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.

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.

The best parts of Rust are Haskell. You've got a lot of precedent for how you do it.
I think I read somewhere that anyhow is great for application code where you want a unified error type across the application. And something like thiserror is good for library code where you want specific error variants for each kind of fallibility.

Personally, I think I prefer thiserror style errors everywhere, but I can see some of the tradeoffs.

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

This already does work in TS, and there are some patterns besides Effect that simplify working with the return values.

Which brings me to my other big gripe with Rust (and Go): the need to declare structs makes it really unwieldy to return many values (resorting to tuples, which make code more error prone and again harder to read).

Fun fact: the compiler itself has some limited inference abilities for return types, they are just not exposed to the language: https://play.rust-lang.org/?version=nightly&mode=debug&editi...

I have some desire to make an RFC for limited cross-item inference within a single crate, but part of it wouldn't be needed with stabilized impl Trait in more positions. For public items I don't think the language will ever allow it, not only due to technical concerns (not wanting global inference causing compile times to explode) but also language design concerns (inferred return types would be a very big footgun around API stability for crate owners).

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

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.

> But yeah I constantly get confused by when errors can and can not coerce.

It's simple, really. `?` coerces errors if there's an `impl From<InnerError> for OuterError`:

    fn outer() -> Result<(), OuterError> {
        inner()?;
        Ok(())
    }
When OuterError is your own type, you can always add that impl. When it's a library type, you're at its mercy. E.g., the point of anyhow::Error is that it's designed to automatically wrap any other error. To do that, anyhow provides an

    impl<E> From<E> for anyhow::Error
    where
        E: Error + Send + Sync + 'static
I regularly get super confused by what coerces and what doesn’t. I swear there’s cases where ? works but into() does not.

It’s many things. But simple ain’t one of them =D

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

(comment deleted)
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?

No, Go didn't get this right. Returning a tuple (a T and an errror) isn't an appropriate tool when you want your function to return either a T or an errror. It's a brittle hack that reqires everyone to use a third-party linter on top. Otherwise that tuple is handled incorrectly too frequently.

All of that, because Go keeps ignoring a basic feature from the 1970s [1] that allows to you express the "or" relationships (and nullability).

APIs that are easy to use incorrectly are bad APIs.

[1]: https://en.wikipedia.org/wiki/Tagged_union#1970s_&_1980s

No it didn't, because it failed to provide an alternative to the boilerplate error checking, like Odin, Rust, Swift and Zig, or monadic composition functions.
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.

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.

> I get pretty far just rolling errors by hand

And you don't punish your compile times.

The macro for everything folks are making Rust slow. If we tire of repetition, I'd honestly prefer checked in code gen. At least we won't repeatedly pay the penalty.

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.

Thanks for using SNAFU! Any feedback you'd like to share?
> multiple error variants that reference the same source error type which I always had issues with in `thiserror`.

Huh?

    #[derive(Debug, thiserror::Error)]
    enum CustomError {
        #[error("failed to open a: {0}")]
        A(std::io::Error),
        #[error("failed to open b: {0}")]
        B(std::io::Error),
    }
    
    fn main() -> Result<(), CustomError> {
        std::fs::read_to_string("a").map_err(CustomError::A)?;
        std::fs::read_to_string("b").map_err(CustomError::B)?;
        Ok(())
    }
If I understand correctly, the main feature of snafu is "merely" reducing the boilerplace when adding context:

    low_level_result.context(ErrorWithContextSnafu { context })?;
    // vs
    low_level_result.map_err(|err| ErrorWithContext { err, context })?;
But to me, the win seems to small to justify the added complexity.
You certainly can use thiserror to accomplish the same goals! However, your example does a little subtle slight-of-hand that you probably didn't mean to and leaves off the enum name (or the `use` statement):

    low_level_result.context(ErrorWithContextSnafu { context })?;
    low_level_result.map_err(|err| CustomError::ErrorWithContext { err, context })?;
Other small details:

- You don't need to move the inner error yourself.

- You don't need to use a closure, which saves a few characters. This is even true in cases where you have a reference and want to store the owned value in the error:

    #[derive(Debug, Snafu)]
    struct DemoError { source: std::io::Error, filename: PathBuf }

    let filename: &Path = todo!();
    result.context(OpenFileSnafu { filename })?; // `context` will change `&Path` to `PathBuf`
- You can choose to capture certain values implicitly, such as a source file location, a backtrace, or your own custom data (the current time, a global-ish request ID, etc.)

----

As an aside:

    #[error("failed to open a: {0}")]
It is now discouraged to include the text of the inner error in the `Display` of the wrapping error. Including it leads to duplicated data when printing out chains of errors in a nicer / structured manner. SNAFU has a few types that work to undo this duplication, but it's better to avoid it in the first place.
The error library he seems looking for is „error_mancer“