Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

multiple error types (struct and enum) and generic IntoError #411

Closed
nerditation opened this issue Nov 27, 2023 · 2 comments
Closed

multiple error types (struct and enum) and generic IntoError #411

nerditation opened this issue Nov 27, 2023 · 2 comments

Comments

@nerditation
Copy link

I encountered problem when writing a program with multiple error types. I have read #199 #399 and #406 which are slightly related but not quite the same.

the tldr

basically, I want to make the context selectors usable for different error types, given one of the types is just (kind of) a transparent wrapper of the other (with an added source field), e.g.

#[derive(Debug, Snafu)]
enum ErrorKind {
    ReadError,
    WriteError,
    ConfigError {
        id: u32
    }
}
#[derive(Debug, Snafu)]
struct MyError {
    kind: ErrorKind,
    source: std::io::Error,
}
fn parse(f: &FIle) -> Result<Config, MyError> {
    // what is currently supported:
    let contents = f.read_all().context(MySnafu { kind: ErrorKind::ReadError })?;
    // or alternatively:
    let contents = f.read_all().context(MySnafu { kind: ReadSnafu.build() })?;
    // what I was hoping for:
    let contents = f.read_all().context(ReadSnafu)?;
    todo!();
}

long text alert:

I started with string contexts for prototyping, something like

enum MyError {
    #[snafu(display("failed to {operation}"))]
    IoError {
        operation: String,
        source: std::io::Error,
    }
}
foo.read(&mut buf).context(IoSnafu { operation: "read file `foo`" })?;
bar.write(&buf).context(IoSnafu { operatoin: "write file `bar`" })?;
baz.sync_data().context(IoSnafu { operation: "flush file `baz`" })?;

after I got a picture of all the error conditions I might need propogate, I started to rewrite with enumerated errors:

enum MyError {
    #[snafu(display("failed {operation}"))]
    UncategorizedIoError {
        operation: String,
        source: std::io::Error,
    },
    ReadFooError {
        source: std::io::Error,
    },
    WriteBarError {
        source: std::io::Error,
    }
}
foo.read(&mut buf).context(ReadFooSnafu)?;
bar.write(&buf).context(WriteBarSnafu)?;
baz.sync_data().context(UncategorizedIoSnafu { operation: "flush file `baz`" })?;

this is much better, but I find out most of my errors have a single type as source (std::io::Error in above example code, git2::Error in the actual program), so I attempt to rewrite it like this:

enum ErrorKind {
    ReadFooError,
    WriteBarError,
    Other {
        operation: String,
    }
}
struct MyError {
    kind: ErrorKind,
    source: std::io::Error,
}
foo.read(&mut buf).context(MySnafu { kind: ReadFooSnafu.build() })?;
bar.write(&buf).context(MySnafu { kind: WriteBarSnafu.build()})?;
baz.sync_data().context(MySnafu { kind: OtherSnafu { operation: "flush file `baz`" }.build() })?;

I'm not very pleased with this, so I looked into the expanded code of derive macro, and came up with:

// enum and struct definition as before
impl snafu::IntoError<MyError> for ReadFooSnafu {
	type Source = std::io::Error;
	fn into_error(self, source: Self::Source) -> MyError {
		MyError {
			kind: self.build(),
			source
		}
	}
}
// usage
foo.read(&mut buf).context(ReadFooSnafu)?;

it works, but I have to manually handle every context selectors, once I try to make it generic, I run into conherence violation (E0210):

// error[E0210]: type parameter `Selector` must be covered by another type when it appears before the first local type (`MyError`)
impl<Selector> snafu::IntoError<MyError> for Selector
where
	Selector: snafu::IntoError<ErrorKind, Source = snafu::NoneError>,
{
	type Source = std::io::Error;
	fn into_error(self, source: Self::Source) -> MyError {
		MyError {
			kind: self.into_error(snafu::NoneError),
			source,
		}
	}
}

I think I understand the cause of the error, but I wonder if I can get some help from the upstream snafu crate, for example, after reading #399 (comment)_ I think that approach might suit my use case, i.e.: NoneError + Selector -> ErrorKind, std::io::Error + Selector -> MyError, in my case, my error types are not generic, so the compile error mentioned in the linked comments should not be an issue.


PS

after writing the above, I realized my use case might be better served by the error-stack crate, where the source chain/stack is a separate concern than the error types themselvs. in the error-stack model, error types don't contain the source chain explicitly, but only contains information about current context of the relevant error, while the source chain is implicitly managed by an external wrapper type (with type erasure). the thing is, I don't like they way how they conflate errors with reports. anyway, I might give that a try.

@shepmaster
Copy link
Owner

most of my errors have a single type as source

This is a big point the point of SNAFU and a major reason it was created; note how the example in the README essentially mirrors yours.

so I attempt to rewrite it

The big missing thing for me here is why?

The closest case I can think of to yours is when I have an error with many detailed variants that I want to categorize into coarser groups. For this, I usually create a separate enum and then an inherent method.

A common example is when reporting an error via HTTP and you want to get a status code:

#[derive(Debug, Snafu)]
enum Error {
    Thing1,
    Thing2,
    Thing3,
}

enum Status {
    Http400,
    Http500,
}

impl Error {
    fn status(&self) -> Status {
        match self {
            Self::Thing1 | Self::Thing2 => Status::Http400,
            Self::Thing3 => Status::Http500,
        }
    }
}

@nerditation
Copy link
Author

most of my errors have a single type as source

This is a big point the point of SNAFU and a major reason it was created; note how the example in the README essentially mirrors yours.

oh, great, I must have read the example in the README looooooong time ago and forgot about it. it indeed is almost the same as my case.

so I attempt to rewrite it

The big missing thing for me here is why?

maybe it's just a personal tastes. in my case, because the git2::Error type already conveys very rich information in it, all I really need is a way to tell me where things went wrong, Location is very handy, but it alone is not enough for me. in fact, I can almost get away with Whatever in most cases, almost. there's still cases I need to match against the error kind.

instead of repeat source: GitError for each variant like this:

use git2::Error as GitError;
#[derive(Debug, Snafu)]
pub enum MyError {
    BbAlreadyExists { source: GitError },
    BaseIndexDirty { source: GitError },
    // many other cases
    WorktreeConflict { source: GitError },
}

I'm more fond of the struct style:

enum ErrorKind {
    BbAlreadyExists,
    BaseIndexDirty,
    // many other cases
    WorktreeConflict,
}
struct MyError {
    kind: ErrorKind,
    source: GitError,
}

my current workaround is to use a custom extension trait for Result, something like this:

pub trait MyResultExt<T> {
	fn kind_context<Kind>(self, kind: Kind) -> Result<T, Error>
	where
		Kind: snafu::IntoError<ErrorKind, Source = snafu::NoneError>;
}
impl<T> MyResultExt<T> for Result<T, GitError> {
	fn kind_context<Kind>(self, kind: Kind) -> Result<T, Error>
	where
		Kind: snafu::IntoError<ErrorKind, Source = snafu::NoneError>,
	{
		self.context(Snafu {
			kind: kind.into_error(snafu::NoneError),
		})
	}
}

at least it solves my problem at hand.

The closest case I can think of to yours is when I have an error with many detailed variants that I want to categorize into coarser groups. For this, I usually create a separate enum and then an inherent method.

A common example is when reporting an error via HTTP and you want to get a status code:

[...]

thanks for the tips. now that I think about it, I probably don't need all those error kind variants in the first place, many of them (which I don't plan to handle, just to report to user) can be put into coarse groups indeed.

I'm gonna close this for now, as it's not really a problem of snafu per se, and I have figured out a workaround too.


an aside:

I played with the error-stack style error handling a bit these days, and I do find it less burden on the brain to just focus on the current error context, and not having to manage the source chain (and backtrace etc). snafu has implicit backtrace capture and location and more, but a source field still must exist somewhere to have a proper source chain. I would think a similar construct could also work in snafu (doesn't need to force type erasure in my opinion), but I don't have a clear idea for now, maybe someday.

Repository owner locked and limited conversation to collaborators Nov 29, 2023
@shepmaster shepmaster converted this issue into discussion #419 Nov 29, 2023

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants