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.
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
sourcefield), e.g.long text alert:
I started with string contexts for prototyping, something like
after I got a picture of all the error conditions I might need propogate, I started to rewrite with enumerated errors:
this is much better, but I find out most of my errors have a single type as source (
std::io::Errorin above example code,git2::Errorin the actual program), so I attempt to rewrite it like this:I'm not very pleased with this, so I looked into the expanded code of derive macro, and came up with:
it works, but I have to manually handle every context selectors, once I try to make it generic, I run into conherence violation (E0210):
I think I understand the cause of the error, but I wonder if I can get some help from the upstream
snafucrate, 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-stackcrate, where the source chain/stack is a separate concern than the error types themselvs. in theerror-stackmodel, 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.