shepmaster / snafu

Easily assign underlying errors into domain-specific errors while adding context
https://docs.rs/snafu/
Apache License 2.0
1.39k stars 60 forks source link

Simple context usage, mysterious compile error #415

Closed vakond closed 9 months ago

vakond commented 9 months ago

Minimal example. I just want to use context:

// Cargo.toml:
// [dependencies]
// snafu = "0.6"

use snafu::{ResultExt, Snafu};

#[derive(Debug, Snafu)]
pub(crate) enum Error {
    #[snafu(display("XXXX '{which}' xxxx"))]
    XXXX { which: String },
}

fn main() {
    let _ = caller();
}

fn caller() -> Result<(), Error> {
    let _ = test().context(XXXX {
        which: "xxxx".to_string(),
    })?;
    Ok(())
}

fn test() -> Result<(), Error> {
    Ok(())
}

But compilation fails with this message:

error[E0271]: type mismatch resolving `<XXXX<String> as IntoError<Error>>::Source == Error`
   --> src/main.rs:15:28
    |
15  |       let _ = test().context(XXXX {
    |  ____________________-------_^
    | |                    |
    | |                    required by a bound introduced by this call
16  | |         which: "xxxx".to_string(),
17  | |     })?;
    | |_____^ expected `Error`, found `NoneError`
    |
note: required by a bound in `snafu::ResultExt::context`
   --> github.com-1ecc6299db9ec823/snafu-0.6.10/src/lib.rs:247:26
    |
247 |         C: IntoError<E2, Source = E>,
    |                          ^^^^^^^^^^ required by this bound in `ResultExt::context`

What's wrong?

shepmaster commented 9 months ago

context, when used on a Result, expects to wrap the inner error. Your error variant doesn't have a source field, so it's trying to use the NoneError which is used when context is used on an Option.

Theoretically you'd want to have something like:

    XXXX { which: String, source: Error },

This cannot work as it makes an infinitely sized type as it contains itself.

More idiomatic SNAFU will have you creating more error types and / or variants. Additionally, you do not need to (and should not) call .to_string() inside of context as it unconditionally allocates memory, even when there's no error.

use snafu::{ResultExt, Snafu};

#[derive(Debug, Snafu)]
enum OuterError {
    #[snafu(display("Xxxx '{which}'"))]
    Xxxx { source: InnerError, which: String },
}

#[derive(Debug, Snafu)]
struct InnerError;

fn main() {
    let _ = caller();
}

fn caller() -> Result<(), OuterError> {
    test().context(Xxxx { which: "xxxx" })
}

fn test() -> Result<(), InnerError> {
    Ok(())
}
vakond commented 9 months ago

Great! Thank you for this excellent explanation. I suspected there should be source, and tried Box<Error> without success. Now I understand why.