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