Skip to content

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

Description

@nerditation

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions