[HN Gopher] Modular Errors in Rust
       ___________________________________________________________________
        
       Modular Errors in Rust
        
       Author : mplanchard
       Score  : 113 points
       Date   : 2023-04-09 14:09 UTC (8 hours ago)
        
 (HTM) web link (sabrinajewson.org)
 (TXT) w3m dump (sabrinajewson.org)
        
       | shepmaster wrote:
       | I encourage people to check out my SNAFU crate [1]. I urge users
       | to create many distinct error types (usually enums but also
       | structs) and compose them -- so much so that I advocate that you
       | never create each specific error in more than one source
       | location. That means that the collection of error types produces
       | a unique trace through your program, uniquely identifying the
       | source of the error with no runtime cost (compare this to a
       | runtime-collected stacktrace / backtrace).
       | 
       | Here's how the final example in the blog post would look like
       | with a quick transform to SNAFU:
       | https://gist.github.com/shepmaster/fb7f4c9519a074ea7186ca7b7....
       | If I spent more time, there'd probably be a bit more changes, but
       | hopefully that gets the idea across.
       | 
       | [1]: https://docs.rs/snafu/
        
         | remram wrote:
         | What for? What do you expect the user of your library to do
         | with this detailed information?
        
           | shepmaster wrote:
           | That's an interesting question that I find difficult to
           | answer. To me, it's as if you are asking "why provide any
           | detail about what happened?". The nonsense end result of that
           | line of logic is that you return a single boolean that
           | corresponds to failure/success, so I assume that's not what
           | you mean.
           | 
           | Contrast the two error messages:
           | 
           | > one end of range is not a valid hexadecimal integer
           | 
           | > the beginning of the range ("0xZZ") is not a valid
           | hexadecimal integer
           | 
           | The latter has more information and points the user to the
           | general area of the problem, and probably allows them to fix
           | the problem themselves. It could be even more improved by
           | providing line/column information.
           | 
           | By splitting out more and more error types (and cases), and
           | artificially limiting yourself to using each error once, you
           | push yourself away from "range is bad" style errors and more
           | specific (and ideally descriptive) errors.
        
             | MaulingMonkey wrote:
             | > The nonsense end result of that line of logic is that you
             | return a single boolean that corresponds to
             | failure/success, so I assume that's not what you mean.
             | 
             | I frequently find that is all that is necessary, and is
             | exactly the correct solution, not "nonsense" whatsoever.
             | Perhaps even that boolean is overkill: an unrecoverable bug
             | should perhaps instead log and then _terminate_ , producing
             | no error condition for the caller of your library to worry
             | about attempting to recover from whatsoever. Detailed error
             | reporting can be done without obscenely distinct error
             | types, and in fact detailed error reporting frequently
             | benefits from a focus on the task itself instead of trying
             | to structure data to leave the task of actually reporting
             | the error to someone else, and kicking the can down the
             | road.
             | 
             | E.g. for parsing focused errors like this I'd be more
             | interested in pretty printing the source line in question,
             | underlining the error, etc. - I wrote
             | https://github.com/MaulingMonkey/json-spanned-value for
             | helping ease the display of bad JSON data in a manner that
             | integrates with your IDE for ease of fixing said data by
             | jumping directly to the cause.
             | 
             | And then, admittedly, there are times when different errors
             | should be recovered from differently. When the caller might
             | wish to retry an operation after some errors, try a
             | _different_ operation after _other_ , report
             | file+line+range information for yet other errors, etc. -
             | these have very concrete answers to "What do you expect the
             | user of your library to do with this detailed information?"
             | that are not difficult to answer at all.
             | 
             | > Contrast the two error messages:
             | 
             | Both are UTF8 strings without types adding anything
             | obviously useful. A silly demo screenshot of errors that
             | will open the offending document if closed, and navigate to
             | the line/column of said error:
             | 
             | https://github.com/MaulingMonkey/json-spanned-
             | value/blob/mas...
             | 
             | Which operates by immediately dumping the "errors" to
             | terminal without _any_ error types involved whatsoever...
             | nor even a boolean branch! Well, a real program might set a
             | boolean so the CLI knows to exit(1) instead of using
             | partially parsed data...
        
             | remram wrote:
             | I'm not saying you shouldn't expose it, but why expose it
             | as distinct error types? Why expose it as a
             | programmatically-accessible information at all?
             | 
             | I can see how the developer debugging the library might
             | want it, but what is the advantage of this information
             | being a type/enum/... over the same information in a
             | string, if the only purpose is for a developer to read it?
        
               | shepmaster wrote:
               | > Why expose it
               | 
               | I read "expose" in two possible ways: (1) as a public- /
               | user-facing API (2) existing at all.
               | 
               | When an error type is a public API that is bound by
               | semver, I do think you should be very careful about what
               | you expose. I suggest starting with an opaque [1] error
               | type and only exposing exactly what you are comfortable
               | with supporting. That may boil down to basically only a
               | string (a.k.a. the `Error` trait).
               | 
               | > Why expose it as a programmatically-accessible
               | information at all?
               | 
               | A few things come to mind...
               | 
               | I'm a huge fan in testing my error cases as much as
               | possible. To that end, a bunch of my tests are
               | semantically `assert_matches!(Err(MyError::Case { .. }),
               | function_result_value)`.
               | 
               | Not all errors are equivalent. For example, if my library
               | fails to read a configuration file, perhaps the caller of
               | the library can recover from that by downloading a file.
               | However, this requires the caller to be able to tell what
               | caused the error.
               | 
               | Interacting with an external API, such as HTTP status or
               | command line exit codes. In this case, you can categorize
               | your errors into domains like "server error" / "client
               | error" / "authorization error".
               | 
               | These could all be done by string matching, but that
               | tends to be comparatively brittle.
               | 
               | > the same information in a string
               | 
               | Aggregating strings like that requires dynamic
               | allocation, which isn't universally available in Rust
               | programs. For example, SNAFU works fine in a `no_std`
               | environment and I know that it's been used in cases like
               | embedded and Windows kernel drivers.
               | 
               | > if the only purpose is for a developer to read it
               | 
               | I don't think that's always true.
               | 
               | [1]: https://docs.rs/snafu/latest/snafu/guide/opaque/inde
               | x.html
        
               | orf wrote:
               | It fits better with the language. It doesn't cost much
               | (anything?) and in simple cases (errors with no dependent
               | fields) returning it is akin to returning an error code.
        
           | shakow wrote:
           | Better understand what happened, and if/how you can fix it.
        
             | remram wrote:
             | I'm not saying why have error reporting at all, I'm asking
             | about this specific approach, for example as opposed to the
             | article being discussed.
        
               | shepmaster wrote:
               | > as opposed to the article being discussed.
               | 
               | I'd actually say that the article and my suggestion are
               | basically compatible. If you check out my gist [1],
               | you'll see that I have basically the same number of user-
               | facing types (or maybe a slightly smaller number as I
               | merged a few structs and enums in some spots).
               | 
               | [1]: https://gist.github.com/shepmaster/fb7f4c9519a074ea7
               | 186ca7b7...
        
           | epistasis wrote:
           | I would expect that the primary use is for those debugging
           | the library. Though this could be users, it would also be
           | developers of the library.
           | 
           | Though, even as a library user, knowing the source of an
           | error can be useful for working around the issue,
           | temporarily, or maybe even figuring out that I was using the
           | library incorrectly.
        
       | nyanpasu64 wrote:
       | > It is thankfully common wisdom nowadays that documentation must
       | be placed as near as possible to the code it documents, and
       | should be fine-grained to a minimal unit of describability (the
       | thing being documented).
       | 
       | I think that documentation which describes each part of a
       | codebase in isolation, without explaining the project's
       | architecture and high-level information and control flow, is a
       | common failure mode of fine-grained "doc comment" based
       | documentation (Doxygen, to an extent Rustdoc, possibly Javadoc
       | but I don't write much Java). I'm looking forward to more
       | projects adopting
       | https://matklad.github.io/2021/02/06/ARCHITECTURE.md.html or
       | similar, particularly to help code contributors who don't already
       | have a maintainer's birds-eye picture of the codebase.
       | 
       | > The codebase becomes more modular. Individual parts can be
       | extracted into different crates or projects if necessary, and
       | strong abstraction boundaries make the code easier to understand
       | in small pieces.
       | 
       | I'd argue that a codebase with too many interacting parts,
       | abstractions or indirections, and possibly excessive modularity,
       | can sometimes result in a system more intimidating to learn than
       | a more straight-line approach (http://number-
       | none.com/blow/john_carmack_on_inlined_code.htm...).
       | 
       | Otherwise I'd largely agree with this post. Another reason to not
       | define the errors returned by a function separately from the
       | function itself, is because of DRY and "every piece of knowledge
       | must have a single, unambiguous, authoritative representation
       | within a system". In this case, allowing a function to return a
       | new error should not require adding that error in a global enum,
       | and separately adding that error return in the actual function
       | located far away from that enum. And finding which function an
       | error value comes from should not require searching the whole
       | codebase for 1 or more occurrences of that value's type.
        
       | loeg wrote:
       | Minor typo near the top: 0x0800 should be 0x0080. Otherwise a
       | very nice article, if you're interested in Rust.
        
       | rectang wrote:
       | A user of a library code such as this sometimes wants to catch
       | all errors related to the library and doesn't care whether they
       | arose from a blocks.ParseError or a blocks.DownloadError or
       | whatever.
       | 
       | I like the granularity of this approach, especially that a
       | function can indicate that it _only_ errors out in some very
       | specific way, such as a blocks.DownloadError.
       | 
       | But how do we get the best of both worlds -- what would be the
       | best way to gather together blocks.ParseError,
       | blocks.DownloadError, etc. into a single blocks.BlocksError type?
        
         | iudqnolq wrote:
         | To a certain extent both approaches are viral. If your library
         | uses the one big enum approach you'll want to add a DepFoo
         | variant, and if you want to expose granular errors you'll need
         | that info from your deps.
         | 
         | I increasingly think the best of both worlds is either extreme,
         | but never the middle ground. Either you actually need very
         | granular errors or you should just use anyhow/eyre and have
         | opaque errors with good human readable descriptions. The middle
         | ground still takes a lot of work, but doesn't seem to give
         | correspondingly better results
        
       | _ZeD_ wrote:
       | Finally people are rediscovering why Java (checked) exceptions
       | were so good :)
        
         | rectang wrote:
         | Rust's error handling is roughly isomorphic to the Platonic
         | ideal of checked exceptions. In both cases you're forced to
         | deal with (recoverable) errors and you know which lines of code
         | can produce (recoverable) errors and which cannot.
         | 
         | However, the problem is that Java didn't implement checked
         | exceptions well, as Joe Duffy articulated.
         | 
         | https://joeduffyblog.com/2016/02/07/the-error-model/#checked...
         | 
         | > _1. Exceptions are used to communicate unrecoverable bugs,
         | like null dereferences, divide-by-zero, etc._
         | 
         | > _2. You don't actually know everything that might be thrown,
         | thanks to our little friend RuntimeException. Because Java uses
         | exceptions for all error conditions - even bugs, per above -
         | the designers realized people would go mad with all those
         | exception specifications. And so they introduced a kind of
         | exception that is unchecked. That is, a method can throw it
         | without declaring it, and so callers can invoke it seamlessly._
         | 
         | > _3. Although signatures declare exception types, there is no
         | indication at callsites what calls might throw._
         | 
         | > _[...]_
         | 
         | The divide in Rust between recoverable Errors and unrecoverable
         | panics is much cleaner (even if it's impossible to get right in
         | every detail).
        
           | illiarian wrote:
           | I tink unrecoverable panics in Rust and Go are a bad design
           | decision. There are very few situations (if any) where you
           | want a completely unrecoverable error. At the very least
           | you'd want to log it somewhere.
           | 
           | No wonder there's a `catch_unwind` in Rust since 1.9 and
           | `set_hook` since 1.10 to try and awkwardly work around this
           | design.
        
         | dllthomas wrote:
         | I think people haven't really wrestled with why Java checked
         | exceptions were so bad. IMO the big problem was a lack of
         | expressiveness. As a simple example, you should be able to
         | write "map" in a way that says "this might throw whatever its
         | argument throws" rather than saying "this can't throw anything"
         | or "this might throw anything" if you want something reasonably
         | generic.
        
       | iudqnolq wrote:
       | I'll definitely be thinking about errors in a new way, so thanks!
       | 
       | > a success story, look no further than the Rust compiler itself;
       | I don't think it would be an exaggeration to say that Rust enjoys
       | the current popularity it does because of how good its error
       | messages are, and how much effort was put into it.
       | 
       | The compiler diagnostics use a very different system than your
       | error types. (eg https://github.com/rust-
       | lang/rust/blob/master/compiler/rustc...).
       | 
       | I think your approach adds a lot of value to a very generic
       | library, but that often all or most of the reasonable users of a
       | library will only propagate. If so, advanced error messages can
       | be a waste of time. For example, a library for writing to the
       | terminal will probably be used by applications that exit with an
       | error message if a write fails, so it's best API might be an
       | opaque wrapper around a string with a very specific error
       | message.
        
         | SabrinaJewson wrote:
         | Absolutely! The right kind of error type really depends on use-
         | case -- I targeted this kind of use case because I see people
         | get it wrong most often, but it's definitely not a one-size-
         | fits-all solution.
        
       | mplanchard wrote:
       | We've been trending toward more localized errors in our codebase
       | at work for a while, largely because of issues like the ones
       | mentioned in this article, although we haven't gotten quite as
       | far as is described here. I'll be adding this to our internal
       | list of articles for rust engineers to read as a part of
       | establishing patterns and idioms!
       | 
       | I do think thiserror can help with a fair bit of the boilerplate,
       | especially when making lots of error types. It'll be interesting
       | to see how we can get the patterns described in this post playing
       | nicely together with it.
        
       ___________________________________________________________________
       (page generated 2023-04-09 23:01 UTC)