multiple error types (struct and enum) and generic IntoError
#419
-
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 tldrbasically, 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 #[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 ( 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 PSafter writing the above, I realized my use case might be better served by the |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments
-
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.
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,
}
}
} |
Beta Was this translation helpful? Give feedback.
-
oh, great, I must have read the example in the
maybe it's just a personal tastes. in my case, because the instead of repeat 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 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.
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 an aside: I played with the |
Beta Was this translation helpful? Give feedback.
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.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 withWhatever
in most cases…