News for hackers. Stuff that matters.
Re:Rust: Enums to Wrap Multiple Errors by shepmaster on Friday October 08, 2021 @11:57AM (#28800680)
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

Re:Rust: Enums to Wrap Multiple Errors by OJFord on Friday October 08, 2021 @02:03PM (#28802194)
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.

Re:Rust: Enums to Wrap Multiple Errors by shepmaster on Friday October 08, 2021 @02:25PM (#28802428)
> 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/comparison/in... [2]: https://crates.io/users/dtolnay
Re:Rust: Enums to Wrap Multiple Errors by OJFord on Friday October 08, 2021 @03:02PM (#28802825)
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).

Re:Rust: Enums to Wrap Multiple Errors by nagisa on Friday October 08, 2021 @02:42PM (#28802597)
`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.
Re:Rust: Enums to Wrap Multiple Errors by shepmaster on Friday October 08, 2021 @02:53PM (#28802729)
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`?

Re:Rust: Enums to Wrap Multiple Errors by nagisa on Friday October 08, 2021 @03:44PM (#28803337)
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.

Re:Rust: Enums to Wrap Multiple Errors by rileyphone on Friday October 08, 2021 @02:38PM (#28802576)
Thanks for the emacs tip! I've been dealing with that for a minute and here is the solution in hn comments, what serendipity.
Re:Rust: Enums to Wrap Multiple Errors by bobbylarrybobby on Friday October 08, 2021 @12:59PM (#28801396)
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`.)
Re:Rust: Enums to Wrap Multiple Errors by shepmaster on Friday October 08, 2021 @01:34PM (#28801795)
> 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. ¯\_(ツ)_/¯

> 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.

Re:Rust: Enums to Wrap Multiple Errors by bobbylarrybobby on Friday October 08, 2021 @03:27PM (#28803123)
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
Re:Rust: Enums to Wrap Multiple Errors by dmix on Friday October 08, 2021 @12:51PM (#28801295)
Kinda feels like putting types in the comments like JSDoc or Dialyzer. I don't use Rust enough to comment otherwise.
Re:Rust: Enums to Wrap Multiple Errors by Measter on Saturday October 09, 2021 @06:35AM (#28808606)
As mentioned, these are attributes not comments. More specifically, the the `#[derive]` attribute on the enum lists which procedural macros to execute (Debug and Snafu here).

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...

Re:Rust: Enums to Wrap Multiple Errors by dmix on Saturday October 09, 2021 @02:20PM (#28811987)
> More specifically, the the `#[derive]` attribute on the enum lists which procedural macros to execute (Debug and Snafu here).

Yep I've used Rust before...

It's still like putting comments above your functions.

Re:Rust: Enums to Wrap Multiple Errors by kalcode on Tuesday October 12, 2021 @11:56AM (#28841296)
> 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.

Re:Rust: Enums to Wrap Multiple Errors by db48x on Friday October 08, 2021 @03:54PM (#28803440)
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.