[HN Gopher] Rust: Enums to Wrap Multiple Errors
       ___________________________________________________________________
        
       Rust: Enums to Wrap Multiple Errors
        
       Author : jasim
       Score  : 81 points
       Date   : 2021-10-08 14:48 UTC (8 hours ago)
        
 (HTM) web link (fettblog.eu)
 (TXT) w3m dump (fettblog.eu)
        
       | quotemstr wrote:
       | More hacks upon hacks to make up for the glaring inability of the
       | Rust people to acknowledge they made a mistake by leaving out
       | exceptions. Just like the "type after identifier" thing, the use
       | of error values instead of exceptions was a pointless piece of Go
       | envy in fashion at the time that we're all going to have to live
       | with for decades.
       | 
       | Plus, Rust doesn't even get to avoid paying for exceptions,
       | because it still has panics and still has to support unwind
       | semantics --- even though they can be disabled at build time.
       | 
       | Avoiding exceptions was a needless technical error and an
       | illustration of why we should design systems around tried and
       | true techniques instead of jumping on fashion trends and
       | ossifying those fashion trends into immutable mediocrity.
        
         | xondono wrote:
         | > they made a mistake by leaving out exceptions
         | 
         | > Just like the "type after identifier" thing
         | 
         | > ossifying those fashion trends into immutable mediocrity.
         | 
         | Can you explain what (in your opinion) was lost here? Because I
         | don't see how this is some kind of critical issue, it just
         | looks like some minor semantics that you just don't like.
         | 
         | I don't see what type of "cost" was payed here that you think
         | implies mediocrity.
         | 
         | Maybe I'm biased because I come from firmware development, but
         | I'd rather have error handling be explicit and as close as
         | possible to the error source.
        
         | shepmaster wrote:
         | Which language(s) did exceptions the best, in your opinion?
        
           | fouric wrote:
           | Not OP, but:
           | 
           | I think that there's a pretty strong case to be made that
           | conditions, implemented in Common Lisp, are the best
           | implementation of "exceptions"...by not actually being
           | exceptions.
           | 
           | Conditions (plus restarts) have two advantages over
           | exceptions (as implemented in almost every other language,
           | including Go and C#) _and_ Rust 's error system that neither
           | can replicate: they allow you to selectively keep the stack
           | wound (both avoiding expensive recomputation and keeping
           | error context), and they separate out multiple error recovery
           | strategies from each other, and allow you to pick which one
           | you want at runtime.
           | 
           | The latter fixes a core problem of all other error-handling
           | systems: that the high-level code usually has the context to
           | determine why a low-level operation was happening, and
           | therefore how the error should be recovered from - but it
           | doesn't (or shouldn't) have knowledge of _what_ low-level
           | operations are happening, because that breaks encapsulation.
           | Meanwhile, the low-level code obviously knows about itself -
           | but it doesn 't have the high-level context necessary to
           | determine which of several error-recovery strategies to take.
           | 
           | A condition system exposes multiple named _restarts_ , which
           | are error-recovery strategies, at the low-level code
           | (technically, you can place them everywhere), and then the
           | higher-level code can choose among those restarts (with names
           | like "abort", "retry", "continue", "ignore") when an error
           | occurs, based on the high-level context and the condition
           | type (because conditions, like exceptions, also have types).
           | 
           | Note that these restarts _do not necessarily unwind the
           | stack_. The stack can remain  "wound" to the point of the
           | error, or it can be partially unwound if a restart a few
           | frames up is selected. Exceptions allow none of this - the
           | moment an exception (or a Rust error type) is
           | thrown/returned, the stack is unwound up to the error-
           | handling point, and all context not explicitly encoded is
           | lost (as well as the possibility of retrying an operation
           | (perhaps with different parameters)).
           | 
           | Short example: let's say you're writing parser for a log
           | format that goes line-by-line. There's the high-level "read-
           | log-file" function, that calls several other functions that
           | eventually calls a "parse-log-file-line" function. From the
           | local perspective of just that low-level function, if it's
           | fed a line it can't parse, there are multiple valid error-
           | recovery strategies: it could try to repair the line as best
           | as it could, it could add the corrupted line to a list, it
           | could skip the line, or it could explode and die. With a
           | normal exception system, you have to either thread a
           | dedicated argument specifying the strategy down to that
           | function, hard-code it to pick a particular strategy, or re-
           | write that function for your particular application. With
           | conditions, the parse-log-file-line function could throw a
           | condition INVALID-LINE and expose each of the previous
           | options as restarts, and the high-level code could pick one
           | of them, or even handle the condition itself!
           | 
           | Longer examples:
           | 
           | https://lisper.in/restarts
           | 
           | https://gigamonkeys.com/book/beyond-exception-handling-
           | condi...
        
             | zozbot234 wrote:
             | The Common Lisp condition system is just an implementation
             | of composable effects, which are useful for plenty of other
             | stuff beyond error handling.
        
               | fouric wrote:
               | It sure would be nice if we could even "just" get them
               | for error-handling...
               | 
               | I didn't know that that's what their real name was. Is
               | there a place that I can read an easy-to-understand
               | explanation of them without a lot of math?
        
           | bartwe wrote:
           | I'd say c#, though the lack of a hard 'nothrow' constraint is
           | annoying.
        
           | quotemstr wrote:
           | Common Lisp, as another commentator described in detail
           | below. Common Lisp is a state of grace from which we have
           | shamefully fallen. It's old out here, east of Eden.
        
         | greydius wrote:
         | Citations needed
        
         | onei wrote:
         | For the type after identifier, the converse is what you see in
         | C family languages. For example:                   // in C
         | int x = ...              // in Rust         let x: i32 = ...
         | 
         | There's a couple of things that Rust can do better than C here.
         | First, is that Rust is easier to parse because you know that
         | let begins a variable declaration. If we look at C, maybe it's
         | a variable, maybe it's a function and you have to get past the
         | identifier to figure that out. The second more user-facing
         | change is that you can omit the type in Rust if you can derive
         | it from somewhere else, e.g. the return type of a function call
         | or a primitive.
        
         | nemothekid wrote:
         | Calling exceptions "Go envy" or a "mistake" seems like naive
         | criticism. Ignoring the fact that the Rust is as old as Go;
         | there is a lot of time spent in building language features with
         | an, from my point of view, academic level of rigor.
         | 
         | There have been a lot of RFCs discussing exceptions in
         | Rust[1][2][3], and the tradeoffs and design have been discussed
         | thoroughly. If you have a design in mind that is safe, robust
         | and performant you are welcome to submit one - as my
         | understanding is the Rust team does not have an ideological
         | slant against exceptions.
         | 
         | I've never understood the hard-on by some C++ fanboys to rush
         | exceptions. You can build (IMO) more reasonable code without
         | abusing ~~gotos~~ exceptions, and a bad exception design (like
         | Java's) is strictly worse than even Go's error handling.
         | 
         | [1] https://github.com/rust-lang/rfcs/pull/243
         | 
         | [2] https://github.com/glaebhoerl/rust-
         | notes/blob/268266e8fbbbfd...
         | 
         | [3] https://github.com/rust-
         | lang/rfcs/blob/master/text/0243-trai...
        
           | pjmlp wrote:
           | The so called "Java design" as urban myth, traces back to
           | CLU, Mesa/Cedar, Modula-2+, Modula-3, and C++.
           | 
           | Naturally the Java designers thought to adopt a feature that
           | was increasingly being made available.
           | 
           | And they only got out of C++ due to the language dialects out
           | there regarding RTTI and exceptions.
        
           | kevinmgranger wrote:
           | Besides, Result types with sufficiently advanced ergonomics
           | might as well be semantically identical to exceptions.
        
         | weavie wrote:
         | With exceptions you still need to decide what you are going to
         | throw. I'm not really seeing how this is a hack that using
         | exceptions would avoid?
        
         | ganbatekudasai wrote:
         | Sum and product types are easier to reason about in total than
         | exceptions. For errors they just integrate into the return type
         | and can be handled as monads. They don't require additional
         | syntax and can directly be passed into other functions.
         | 
         | They have existed since (at least) the early 70s.
        
       | shaoner wrote:
       | I faced the same kind of issue lately and thought that
       | implementing a From trait for each type of error was kind of
       | annoying.
       | 
       | Taking the article example, I ended up doing this:
       | #[derive(Debug, Clone, Copy, Eq, PartialEq)]         pub enum
       | MyError {             MyErr1,             MyErr2,
       | MyErr3,         }              fn read_number_from_file(filename:
       | &str) -> Result<u64, MyError> {             let mut file =
       | File::open(filename).or(Err(MyError::MyErr1))?; // Error!
       | let mut buffer = String::new();
       | file.read_to_string(&mut buffer).or(Err(MyError::MyErr2))?; //
       | Error                  let parsed: u64 =
       | buffer.trim().parse().or(Err(MyError::MyErr3))?; // Error
       | Ok(parsed)         }
       | 
       | As I'm a beginner, I would love to hear some thoughts on this
        
         | onei wrote:
         | I really like the thiserror crate for cutting down boilerplate
         | and you can add annotations to automatically implement From.
         | However, if you want to handle errors with more granularity,
         | e.g. IO errors as noted elsewhere in the discussion, then
         | you'll need to implement From yourself.
         | 
         | And as others noted, .map_err(...) is slightly better than
         | .or(...) for wrapping errors.
        
         | loeg wrote:
         | The downside of this approach is that you have discarded the
         | original error information. It's also somewhat verbose at the
         | error-handling site.
        
         | sfvisser wrote:
         | Nothing wrong with that, although you could use 'map_err'
         | instead of 'or' to preserve the original errors as well if you
         | want to.
        
         | hota_mazi wrote:
         | The only downside of this approach is that you can't pass a
         | dynamic string giving details on the error, but that's because
         | Rust's enums are not powerful enough to express this (as
         | opposed to Kotlin or Java).
        
       | nagisa wrote:
       | The fact that failure conditions from both `File::open` and
       | `read_to_string` become an `IoError` is a significant roadblock
       | to making these errors useful. The mechanism as described in this
       | blog post also fails to introduce contextual information about
       | the reason a failure has occurred.
       | 
       | This means that errors, if implemented as described in this post,
       | either formatted or handled don't give sufficient information to
       | the caller/user on how to deal with the error.
       | 
       | EDIT:
       | 
       | I strongly recommend to use `op.map_err(|e| SomeError::Open(e,
       | filename))?` and `op.map_err(SomeError::Read)?` as an alternative
       | when propagating the errors. It is more typing at the location of
       | propagation than just `?`, but the errors this approach produces
       | are immediately actionable regardless of whether they are printed
       | to the user or handled by a caller. Provided, of course, this
       | pattern is applied consistently.
        
         | shepmaster wrote:
         | Check out my sibling comment about SNAFU, which addresses your
         | concerns, if I understand them.
        
         | matklad wrote:
         | Heh, I opened the comment section to link
         | https://kazlauskas.me/entries/errors.html and
         | https://sled.rs/errors.html :-)
        
         | Macha wrote:
         | You can match on std::io::ErrorKind, which is available by
         | calling kind() on your error. This effectively has the same
         | result as error inheritance hierarchies in Java and C#.
         | 
         | I don't think you really would treat a failure in File::open
         | and read_to_string() differently in most applications. If you
         | have a function doing file IO you might retry reading a
         | read_to_string() error, but you know that the file exists
         | because you know which function you just called.
         | 
         | If you're at higher level of abstraction, where something is
         | calling both open() and read_to_string() for you, are you
         | really going to handle it differently that frequently whether
         | it's a missing file or a file on an unavailable storage medium?
        
           | nagisa wrote:
           | In my personal experience distinct handling of errors like
           | these does indeed come up significantly more rarely. What
           | ends up mattering to me much more often is how these errors
           | are presented to the user, or what ends up in the logs in
           | production. In those cases something like
           | error: failed to read contents of `foo`           caused by:
           | a physical I/O error has occurred (...)
           | 
           | and                   error: failed to open `foo`
           | caused by: permission denied (...)
           | 
           | are significantly more actionable than just the underlying
           | I/O errors by themselves. To the point where a person reading
           | these messages has a shot at addressing the problem without
           | any prior knowledge of the code base.
           | 
           | In `anyhow` adding such context via its `context` family of
           | methods seems fairly well accepted. I hope that my comment
           | demonstrates there's no reason why `enum` based approach
           | would need to be any worse than what `anyhow` can achieve.
        
             | Macha wrote:
             | For your logs you're going to print the error's default
             | message anyway, so you should have the underlying message
             | of the std::io::Error as long as it's listed in the source
             | of your wrapped error.
        
               | shepmaster wrote:
               | > you should have the underlying message of the
               | std::io::Error
               | 
               | This is a point of debate[1] among the error-handling
               | working group.
               | 
               | [1]: https://github.com/rust-lang/project-error-
               | handling/issues/4...
        
             | shepmaster wrote:
             | > how these errors are presented to the user, or what ends
             | up in the logs in production. In those cases something like
             | [...] are significantly more actionable than just the
             | underlying I/O errors by themselves. To the point **where a
             | person reading these messages has a shot at addressing the
             | problem** without any prior knowledge of the code base.
             | 
             | (emphasis mine)
             | 
             | This is a _huge_ belief of mine as well. I call it a
             | "semantic stacktrace".
             | 
             | I actually go as far as to say that _most of the time_
             | backtraces in errors are an antipattern, as it gives a
             | false sense that an error is actionable ("oh, just look in
             | the code!").
             | 
             | I even try to make it such that each leaf error is
             | constructed in exactly one place so that grepping for the
             | error identifies that one location.
        
       | Macha wrote:
       | For reducing the code to implement this pattern, `thiserror` is
       | nice. `derive_more` also has similar functionality, and I tend to
       | use that more personally because I use its other derives, and
       | while thiserror has a slightly nicer API (derive_more will assume
       | a unit struct's field is it's source, which is wrong more often
       | than not in my codebase, since I avoid overdoing nested error
       | enums), it's not worth adding another dep to do what the dep I
       | already have can do:
       | https://jeltef.github.io/derive_more/derive_more/error.html
        
         | stormbrew wrote:
         | huh that's cool. If it also had a "better derive(Default)" as
         | well I'd probably end up using this in almost everything I
         | write tbh.
        
           | onei wrote:
           | The downside to derive_more from what I can tell is that it
           | impacts compile times fairly noticeably compared to the
           | equivalent handwritten code. YMMV, but I've seen it removed
           | from some crates for that specific reason.
        
       | c7DJTLrn wrote:
       | Error handling in Rust is one of the reasons I went back to Go.
       | It's a lot of mental overhead having to constantly think about
       | all the different types of errors from different crates that
       | could be returned and how to handle them at the caller. I don't
       | want to have to import somebody's hobby crate that claims to make
       | it easier either. In Go it's just dead simple.
       | 
       | I've noticed a lot of Rust discussion is on how to write the code
       | or make the compiler happy which is quite telling. I don't want
       | to spend my time thinking about how to write the code and make
       | the compiler happy - I want to get a problem solved and a ticket
       | closed. Hence why I went back to Go (but I acknowledge these
       | languages have distinct goals).
        
         | TheDong wrote:
         | I've found that Go has significantly more mental overhead when
         | it comes to error handling than rust does.
         | 
         | In Go, I don't have type information to know what kind of error
         | it is. In rust, I can typically just "match err", and let the
         | compiler tell me what error types it could be, and then handle
         | each of them.
         | 
         | In go, I always have to read the entire function that returns
         | the error, and any functions it calls, and so on, and then I
         | have to think about how to handle it.
         | 
         | Is it an error from the os package, like "os.Open"? Well, maybe
         | I need:                   if os.IsNotExist(err) { /* handle */
         | }
         | 
         | Is it an error from the net package? I might need:
         | var addrErr *net.AddrError         var invalidAddrErr
         | net.InvalidAddrError         var otherErr net.Error         if
         | errors.As(err, &addrErr) {           /* handle addrErr */
         | } else if errors.As(err, &invalidAddrErr) {           /* handle
         | invalidAddrErr */         } else if errors.As(err, &otherErr) {
         | /* some other net error */         } else if errors.Is(err,
         | net.ErrClosed) {           /* handle this network error which
         | is _not_ a net.Error */         }
         | 
         | Is it an error that used one of the third party error
         | libraries, like "github.com/hashicorp/errwrap"? I may need even
         | more special handling.
         | 
         | The fact that "errors.Is" and "errors.As" requires me to know
         | if an error is a "sentinel" one like 'net.ErrClosed', or a
         | struct error, like 'os.PathError', and I have to know if the
         | struct is implementing error on a pointer receiver or not is
         | also really annoying since it means I have to constantly go
         | check error definitions, even if I vaguely remember what errors
         | a function returns.
         | 
         | And, what's worse, the type system doesn't help me! It won't
         | tell me if I get any of these things wrong. I can use
         | "errors.Is" and "errors.As" backwards, and the type system
         | won't help. I have no way to determine what errors a function
         | can return without reading hundreds of lines of code.
         | 
         | Frankly, Go's error handling has more overhead than almost any
         | other language, and I find it surprising you find it simple.
         | 
         | If all I'm doing is bubbling errors up, then rust's "?"
         | operator works fine, and is easier than Go's "if err != nil"
         | boilerplate. If I'm handling errors, rust's features are
         | helpful, and Go is so much mental overhead it usually makes me
         | forget the problem I was trying to solve by the time I've drawn
         | out the full diagram of possible errors something can return up
         | the stack. Usually rust's type-system has enough info that I
         | don't need to read any code to know what errors I need to
         | handle. That's never the case in Go, because idiomatically
         | errors are "type-erased" into the error interface.
         | 
         | Oh, and this is all not to mention that go facilitates making
         | unhandle-able errors. If someone returns "errors.New" or
         | "fmt.Errorf", you're stuck with string matching, while rust
         | encourages making typed and handle-able errors. The number of
         | times I've had my go code break because someone reworded an un-
         | exported error string I had to match on is pretty high. Hasn't
         | happened to me in rust yet.
        
         | iknowstuff wrote:
         | Your reluctance to use a crate which solves the issue you are
         | facing is somewhat antithetical to Rust's crate-centric
         | philosophy. After all, anyhow::Result does "get your problem
         | solved and ticket closed" just the way you want.
         | 
         | That being said, in reality it's probably a pretty common
         | sentiment among adopters, especially since the macro crates can
         | slow down compilation. Thankfully, the ergonomics of error
         | handling are indeed under consideration and work is underway to
         | make it all better.
        
       | aazaa wrote:
       | I find this kind of article that explains how to do something
       | with vanilla Rust valuable because many articles will instead
       | explain how to use a crate that offers similar functionality.
       | This is the only way to know if a crate can actually pull its
       | weight before you decide to adopt it.
        
       | schneems wrote:
       | Errors in rust are one of the hardest things for me to wrap my
       | head around as a beginner. Thanks for this post!
       | 
       | I also really like this blog post that also walks you through the
       | history of some features like the try! Macro and shows how they
       | are implemented https://blog.burntsushi.net/rust-error-handling/
        
       | dljsjr wrote:
       | Propagation without needing to use boxed trait objects can also
       | be accomplished using `anyhow`[1]/`eyre`[2], which have nice
       | downcasting API's for recovering the original error type if you
       | know what the possibilites could be. I only bring it up because
       | they aren't mentioned until the end of the article and only in
       | passing but they offer really nice features for attaching context
       | and downcasting that makes up for the pseudo-type-erasure.
       | 
       | 1: https://github.com/dtolnay/anyhow
       | 
       | 2: https://github.com/yaahc/eyre
        
         | shepmaster wrote:
         | > Propagation without needing to use boxed trait objects
         | 
         | You can also use enums, as shown in the post. You don't need
         | trait objects.
         | 
         | EDIT 1
         | 
         | > without needing to use boxed trait objects [...] `anyhow`
         | 
         | It's my understanding that anyhow uses trait objects. The first
         | sentence of its document says "This library provides
         | anyhow::Error, a trait object based error type"
         | 
         | EDIT 1 END
         | 
         | > which have nice downcasting API
         | 
         | I'm not a fan of downcasting when not _absolutely_ necessary.
         | 
         | See my sibling comment about SNAFU for an alternate that allows
         | keeping different errors separate while unifying them.
        
           | dljsjr wrote:
           | Sure but I've also looked at projects that have 15-20 enums
           | (or more) for their error types and it makes things very
           | cumbersome. `anyhow` makes it painless to have arbitrary
           | errors. It's trying to take the context and stuff it in to
           | the type system. Which isn't necessarily a bad idea but it
           | can become unwieldy.
           | 
           | I've done both in production projects and they both have
           | their merits but 9 out of 10 times I'll start with `anyhow`
           | if I'm writing library code and then refactor to enums later
           | if I need it instead of the other way around.
           | 
           | EDIT: That said Snafu does look really cool for the cases
           | where you do have a codified sum type of all possible errors.
           | 
           | DOUBLE EDIT: I already had Snafu starred on GH lol
        
             | shepmaster wrote:
             | > 15-20 enums (or more) for their error types and it makes
             | things very cumbersome
             | 
             | Like everything, it can be a balancing act. I tend to be
             | free about creating error types (one per module, usually,
             | but it's not strange for me to create more). When you
             | implement `std::error::Error` (to be able to make error
             | trait objects) and have a tool like SNAFU (to make error
             | enums), then having more error types isn't much of a
             | hindrance in my experience.
             | 
             | > I'll start with `anyhow` if I'm writing library code and
             | then refactor to enums later
             | 
             | The newest (beta) version of SNAFU actually strives to make
             | this case even smoother. There's a `whatever!` macro that
             | you can use to create stringly-typed errors and then
             | migrate case-by-case to an enum. Check out the docs for
             | some example usage.
        
               | dljsjr wrote:
               | I used the wrong wording: They have 15-20 *enum variants*
               | per enum definition. Or more. I worked with a project
               | that had close to 30-something enum variants.
               | 
               | That's just... wildly annoying to deal with IMO.
        
               | catlifeonmars wrote:
               | That sounds like a different design issue. Errors are
               | certainly part of the API and it looks like they're not
               | encapsulated well enough.
        
       | shepmaster wrote:
       | I'll hype my own library, SNAFU [1].
       | 
       | It simplifies constructing your own "leaf" errors and streamlines
       | the ability of collecting multiple types of errors while
       | attaching more context to them (e.g. filenames, stack traces,
       | user ids, etc.). It allows you to smoothly switch from "stringly-
       | typed" errors to strongly-typed errors. You can create opaque
       | errors to avoid leaking internal implementation details into your
       | public API.
       | 
       | Applied to the code in the post:                   use
       | snafu::prelude::*;         use std::{fs::File, io::prelude::*};
       | #[derive(Debug, Snafu)]         enum Error {
       | #[snafu(display("Unable to open {filename}"))]
       | Opening {                 source: std::io::Error,
       | filename: String,             },
       | #[snafu(display("Unable to read {filename}"))]
       | Reading {                 source: std::io::Error,
       | filename: String,             },
       | #[snafu(display("Unable to parse {buffer} as a number"))]
       | Parsing {                 source: std::num::ParseIntError,
       | buffer: String,             },         }                  fn
       | read_number_from_file(filename: &str) -> Result<u64, Error> {
       | let mut file = File::open(filename).context(OpeningSnafu {
       | filename })?;                      let mut buffer =
       | String::new();                      file.read_to_string(&mut
       | buffer)                 .context(ReadingSnafu { filename })?;
       | let buffer = buffer.trim();             let parsed: u64 =
       | buffer.parse().context(ParsingSnafu { buffer })?;
       | Ok(parsed)         }
       | 
       | The key parts are the `derive(Snafu)` on the definition of the
       | error enum and the usages of `.context` and `XxxSnafu` at the
       | error sites.
       | 
       | Importantly, this example demonstrates a key feature of SNAFU,
       | here shown as "not all `io::Error`s are the same". Opening the
       | file and reading the file are two separate error conditions and
       | should _not_ be lumped together as one.
       | 
       | [1]: https://docs.rs/snafu/0.7.0-beta.1/snafu/index.html
        
         | dmix wrote:
         | Kinda feels like putting types in the comments like JSDoc or
         | Dialyzer. I don't use Rust enough to comment otherwise.
        
           | db48x wrote:
           | It's a little similar, except that these are attributes
           | rather than comments. They're part of the syntax of the
           | language, and new macros can be written to add new
           | attributes. Attributes are used for several different
           | purposes within the language, so using them is familiar and
           | common.
           | 
           | Rust also has two kinds of comments, one that shows up in the
           | generated documentation and one that doesn't.
        
         | bobbylarrybobby wrote:
         | Seems cool, but is a whole crate worth it for the `.context`
         | function when you could just use e.g., `.map_err(|source|
         | Error::Opening { source, filename })`? Seems like all
         | `.context` provides is not not needing to name the originating
         | error? (And obviously the `#[snafu(display(...))]` macros could
         | just be moved into a `impl Debug for Error`.)
        
           | shepmaster wrote:
           | > but is a whole crate worth it
           | 
           | Yes. I'm not sure exactly what other response you'd expect
           | from the author/maintainer of a library when they've already
           | made a post encouraging other people to use it. -\\_(tsu)_/-
           | 
           | > when you could just use e.g., `.map_err(|source|
           | Error::Opening { source, filename })`
           | 
           | That's not equivalent, as `filename` is a `&str` but becomes
           | a `String` when stored in the error. SNAFU automatically
           | calls `Into::into` for you, so the closest would be:
           | .map_err(|source| Error::Opening { source, filename:
           | filename.into() })
           | 
           | > Seems like all `.context` provides
           | 
           | There's also the possibility of automatic construction of
           | values (backtraces, location information, the current time,
           | things captured from globals / thread locals, etc.)
           | 
           | Beyond `.context`, SNAFU also implements the `Error` trait
           | (and associated methods like `Error::source`).
           | 
           | There's also convenience methods and macros to create leaf
           | errors, those that originate in your code.
           | 
           | > obviously the `#[snafu(display(...))]` macros could just be
           | moved into a `impl Debug for Error`
           | 
           | I'll assume you mean `Display`, not `Debug`. That also not
           | quite true, as SNAFU offers a shorthand syntax that isn't yet
           | in stable Rust:                   "Unable to read {filename}"
           | 
           | would need to be one of                  "Unable to read {}",
           | filename        "Unable to read {filename}", filename =
           | filename
           | 
           | > like all [...] provides [...] obviously [..] could just be
           | moved
           | 
           | All code could have been written by your own hand or
           | otherwise inlined. Your response feels (needlessly) highly
           | dismissive of another person's work.
        
             | bobbylarrybobby wrote:
             | Thanks for your reply. I guess my question should really
             | have been a statement: "based on this example, I don't
             | think it's worth it". But the info you provided does make
             | it seem worthwhile. Cheers
        
         | OJFord wrote:
         | Obviously I realise which you'll say is best, but any comment
         | on snafu vs thiserror/anyhow?
         | 
         | That's what I've used so far pretty much just because it seemed
         | the popularly recommended way to solve the problem, but I
         | wouldn't say it's been massively smooth.
         | 
         | Also, it seems unfortunate you won't get autocompletion (the
         | first time anyway) for (the 'Snafu') part of XxxSnafu.
        
           | shepmaster wrote:
           | > snafu vs thiserror/anyhow
           | 
           | I'd like to provide a fair comparison [1] in the
           | documentation, but I don't know thiserror / anyhow well
           | enough to feel like I'd give them the credit they are due.
           | 
           | That said, to _my knowledge_ , thiserror doesn't allow you to
           | take an `io::Error` and sort it into two different enum
           | variants (like the `Reading` and `Opening` variants in my
           | grandparent example). To me, those are vastly different error
           | states that just both happen to have the same error type. You
           | can extend the metaphor with any larger error type from a
           | crate (e.g. `reqwest::Error`).
           | 
           | Anyhow requires using a trait object (and potentially
           | downcasting) and I prefer avoiding those when possible.
           | 
           | > seemed the popularly recommended way
           | 
           | Absolutely. The author of those crates is a _giant_ in the
           | Rust community [2] (they are also the author of serde, syn,
           | and quote, for example!). If those crates suit your
           | situations, then by all means -- use them. I 'd much rather
           | the Rust community have better error types and messages by
           | whatever means available. Even using `String` via `Box<dyn
           | Error>` is better than nothing.
           | 
           | > you won't get autocompletion
           | 
           | You should, at least if you use rust-analyzer. I use it via
           | emacs and have these settings enabled, but I _think_ they
           | were going to be the default:                   (lsp-rust-
           | analyzer-cargo-load-out-dirs-from-check t)         (lsp-rust-
           | analyzer-proc-macro-enable t)
           | 
           | [1]: https://docs.rs/snafu/0.7.0-beta.1/snafu/guide/compariso
           | n/in... [2]: https://crates.io/users/dtolnay
        
             | OJFord wrote:
             | Thanks!
             | 
             | > To my knowledge, thiserror doesn't allow you to take an
             | `io::Error` and sort it into two different enum variants
             | (like the `Reading` and `Opening` variants in my
             | grandparent example)
             | 
             | Indeed that does look nice, I don't know for sure either,
             | I'm only very basically using it. (Which is where it's been
             | a bit of a pain at times - to be honest for where I've been
             | using it so far I just wanted a dead simple 'I really don't
             | care just return the basic error message in some way that
             | compiles'.)
             | 
             | > The author of those crates is a giant in the Rust
             | community [dtolnay]
             | 
             | For what it's worth, I haven't conducted any sort of
             | objective metric-based comparison obviously, but I
             | recognise both of your usernames equally as giants. ;)
             | 
             | > You should [get autocompletion on *Snafu], at least if
             | you use rust-analyzer
             | 
             | Oh, clever. "[rust-analyzer] is a part of a larger rls-2.0
             | effort to create excellent IDE support for Rust." I'll have
             | to check, but I'm pretty sure I'm using 'rls-1.0' (in vim).
        
             | rileyphone wrote:
             | Thanks for the emacs tip! I've been dealing with that for a
             | minute and here is the solution in hn comments, what
             | serendipity.
        
             | nagisa wrote:
             | `thiserror` definitely does allow it, e.g.
             | #[derive(thiserror::Error, Debug)]         pub enum Error {
             | #[error("Cannot open `{1}`")]
             | OpenFile(#[source] std::io::Error, PathBuf)
             | #[error("Cannot read file contents")]
             | ReadFileContents(#[source] std::io::Error)
             | #[error("Cannot parse the configuration file")]
             | ParseConfig(#[source] serde::Error)         }
             | fn open_config(path: &Path) -> Result<Config, Error> {
             | let mut file = File::open(path).map_err(|e|
             | Error::OpenFile(e, path.to_owned()))?;             let mut
             | data = Vec::with_capacity(1024);
             | file.read_to_end(&mut
             | data).map_err(Error::ReadFileContents)?;
             | parse_config(&data).map_err(Error::ParseConfig)         }
             | 
             | EDIT: adjusted to add an example of adding path to the
             | error message.
        
               | shepmaster wrote:
               | I should have worded myself better, apologies.
               | 
               | With that example, since you use `Result::map_err` and
               | specify the specific variant, you aren't really using
               | anything _from thiserror_ at the site of the `?`,
               | correct?
               | 
               | Most usages I have seen, people use `#[from]`, which
               | would end up having conflicting implementations. Is there
               | a reason you didn't use `#[from]` for `ParseConfig`?
        
               | nagisa wrote:
               | Ah, I see.
               | 
               | In short, I consider `From::from` implementations for
               | errors to be an anti-pattern. It is super easy to become
               | lax about adding context with these implementations in
               | place. Especially as code is modified in the future.
               | 
               | I describe the approach that I use for errors in a detail
               | in an article (https://kazlauskas.me/entries/errors.html)
               | that has already been linked elsewhere in the thread. It
               | is, as far as I can tell, pretty much equivalent to what
               | `snafu` makes users to do.
        
       | ejanus wrote:
       | Beyond Borrow checker, move/copy, and error propagation(different
       | error in fn), what else should someone learn in order to have
       | basic understanding of Rust?
        
       ___________________________________________________________________
       (page generated 2021-10-08 23:01 UTC)