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.
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/comparison/in...
[2]: https://crates.io/users/dtolnay> 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).
#[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.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`?
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.
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. ¯\_(ツ)_/¯
> 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` providesThere'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 movedAll code could have been written by your own hand or otherwise inlined. Your response feels (needlessly) highly dismissive of another person's work.
These procedural macros are handed the token stream for that enum, and can use that to generate new code. The Snafu macro here is also using attributes on each variant of the enum for information about how to implement certain things.
The documentation for Snafu has a page[1] describing what code is generated. There's nothing there that couldn't be done by hand, it's just tedious.
[1] https://docs.rs/snafu/0.6.10/snafu/guide/the_macro/index.htm...
Yep I've used Rust before...
It's still like putting comments above your functions.
If you're used to a certain language then sure that makes sense. Your comment seems like you are boxing yourself in, limiting yourself to just what makes sense in Javascript, very opposite of a programmer who looks to improve their craft.
Take a moment and think about that. Because it looks like Javascript comment...you only see it like that to a point of making this statement. There is a lot of ways different languages uses tokens/symbols to indicate something. Not everyone agrees what those symbols are used for. Some languages use it to define macros like C++ or preprocessor directives like C#. Some as comments JS/Python/. Some as like Java nothing.
Because the languages you use are use to it, and the language you use doesn't use something similar, it looks "wrong". That is in itself a narrow view.
I think you should question that kind of thinking, I believe it will be helpful.
Rust also has two kinds of comments, one that shows up in the generated documentation and one that doesn’t.
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:
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