1. 21
    1. 8

      I’ve been writing Rust full-time with a small team for over a year now.

      It sounds you have been building an application, rather than a library with semver-guarded API. This explains the differences:

      • in a library, taking a proc macro dependency significantly restructures compilation graph for your consumer, and you, as a library, don’t want to force that. In an application, you control the compilation graph, and adding a proc macro might not be problematic.
      • libraries should work hard on protecting their own abstractions. From only matters when you raise an error, user of the library has no business raising errors. Adding From publicly exposes the internal details of how you use ?. In an apppicarion, you don’t need to protect abstractions as hard, as you can just refactor the code when issues are discovered

      (Personally, I’d am not a fan of using From even in applications, as it makes it harder to grep for origins of errors)

      1. 5

        I’ve been building both applications and libraries. And I 100% agree with both of your points wrt. libraries.

        But, I don’t think the application / library binary is … well … binary. It’s a spectrum.

        In my experience, any application of significant complexity will have internal library-like things. In that kind of situation, protecting their abstractions is vital but a slightly more complex compilation graph less so. (👀 tokio and friends)

        Conversely, a library might exist to make hard things easier. One common case is an abstraction that intentionally exposes its internals to eliminate complexity for the callers.

        (wrt. From in applications and grepping for the origins of errors, I blame Rust’s error handling ecosystem’s anemic support for backtraces and locations. We shouldn’t have to grep! I touch on that at the end of the gist. I’ve daydreamed about thiserror supporting a location field that does something similar to this hack.)

        1. 1

          (Personally, I’d am not a fan of using From even in applications, as it makes it harder to grep for origins of errors)

          Wow. I thought I was the only one. I’m very skeptical of implementing From for error types. At least as a general pattern.

          The main reason I’m skeptical of it is because (obviously) From works based on the type of the value, but not on the context. As a hypothetical example, your code might encounter an std::io::Error during many different operations; some may be a fatal error that calls for a panic, some may be a recoverable/retryable operation, and some may be a business logic error that should be signaled to the caller via a domain-specific error type. When you implement From for a bunch of third-party error types and 99% of your “error handling” is just adding a ? at the end of every function call, it’s really easy to forget that some errors need to actually be handled or inspected.

      2. 2

        in a library, taking a proc macro dependency significantly restructures compilation graph for your consumer, and you, as a library, don’t want to force that.

        hmmm I’m not sure what the issue is here. Dependencies have dependencies, I don’t think people find that surprising. For me it’s more of a question of how many dependencies and how does that set of dependencies change my tooling?

        A proc macro that only depends on the built-in proc_macro crate is pretty different from a proc macro that depends on syn, quote, and proc_macro2.

        1. 3

          proc macro deps are different, for two reasons:

          • they are not pipelined. proc macro crate must be compiled to .so before dependant crates can even start compiling (normally, cargo kicks downstream compilation as soon as .rmeta is ready)
          • half the world depends on syn, so, if you have many cores, it normally is the case that one core compiles syn while the others are twiddling their thumbs.

          And yes, this mostly doesn’t apply if the proc macro itself is very small and doesn’t depend on parsing Rust code/syn, but thiserror uses syn.

          1. 1

            What kind of macros are compile-time efficient, if any?

            1. 3

              In general, macros that avoid parsing Rust and instead use their own DSL. See eg this bench: https://github.com/matklad/xshell/blob/af75dd49c3bb78887aa29626b1db0b9d0a202946/tests/compile_time.rs#L6

          2. 1

            ah. I personally don’t like syn so I get the gripe. I think “avoid proc macros in libraries” is casting a wide net, but “you probably don’t need syn to write that proc macro” is something I very much agree with.

    2. 3

      TIL about testresult :^)

      I love how elegant this library is. I encourage that you go look at the source code, but the TL;DR is: it panics in a From implementation with #[track_caller] fn from(..).

      ? automatically calls From::from on the Err of the result you’re trying to bubble up, so bubbling up any error from a function returning TestResult will end up calling <TestError as From>::from, which means that any time you try to bubble up an error it’ll panic with the location of ?. Very very cool.

      I’m actually wondering if it could be used to produce something akin to Zig’s error return traces, where each time you bubble up an error it would record the callsite location inside the Err variant.

      1. 5

        I’m actually wondering if it could be used to produce something akin to Zig’s error return traces, where each time you bubble up an error it would record the callsite location inside the Err variant.

        Inspired by this post, I am literally spiking that right now. 🥸

        Edit: yes, it’s quite possible

      2. 1

        each time you bubble up an error it would record the callsite location inside the Err variant.

        I’m not familiar with Zig; is snafu::Location equivalent? Maybe Zig does it with less boilerplate.

    3. 2

      I don’t agree with the “rename kind to source” bit.

      ErrorKinds are meant to be plain enums showing the kind of error it is. source is meant to be the underlying error that produced the wrapping error. As an example, io::ErrorKind is that plain enum.

      I would argue that ParseErrorKind::ParseInt wrapping another error makes it not a Kind.

      The pattern I’ve seen is that Kinds go alongside errors or other info, to disambiguate them. But an enum that contains error data is just an Error.

    4. 1

      Really nice post. The implicit conversion code is prettier. One thing I’m wrestling with: implicit conversions are better for long term ergonomics but can be challenging for new devs. Especially when you have several that happen at once and further obscured if using a custom defined Result.

      Not specific to the post, just something I’m thinking on.

      the most obvious of which is I named the closures

      It took me a bit to realize that “naming” means assigning to a variable then calling the function/variable.

      1. 1

        It took me a bit to realize that “naming” means assigning to a variable then calling the function/variable.

        Thank you, I’ve update the gist with your wording!

        1. 1

          Thanks! It’s a neat technique.