SNAFU 0.2.1 Released

I would not consider myself to have throughly used Failure, so I'm hesitant to provide a head-to-head comparison due to the risk of being biased (because obviously SNAFU is better :wink:).

Here's my thoughts on these points:

  • Strings as errors

If you want this, then you might as well just use Box<dyn Error>:

fn example() -> Result<(), Box<dyn std::error::Error>> {
    Err(format!("Something went bad: {}", 1 + 1))?;
    Ok(())
}

I don't see the benefit that Failure provides here. If you wanted to do something similar with SNAFU, I'd say something like:

#[derive(Debug, Snafu)]
enum Error {
    #[snafu(display("Something went bad: {}", value))]
    WentWrong { value: i32 },
}

fn example() -> Result<(), Error> {
    WentWrong { value: 1 + 1 }.fail()?;
    Ok(())
}
  • A Custom Fail type
  • Using the Error type

These two forms are the bread-and-butter of SNAFU, and appear to avoid the downsides listed in the guide: you don't have to have a 1-1 relationship between underlying error and error variant, it's not required to allocate, and you can pass along any extra information that you need:

#[derive(Debug, Snafu)]
enum Error {
    #[snafu(display(r#"Could not parse the area code from "{}": {}"#, value, source))]
    AreaCodeInvalid {
        value: String,
        source: ParseIntError,
    },
    #[snafu(display(r#"Could not parse the phone exchange from "{}": {}"#, value, source))]
    PhoneExchangeInvalid {
        value: String,
        source: ParseIntError,
    },
}

fn example(area_code: &str, exchange: &str) -> Result<(), Error> {
    let area_code: i32 = area_code
        .parse()
        .context(AreaCodeInvalid { value: area_code })?;
    let exchange: i32 = exchange
        .parse()
        .context(PhoneExchangeInvalid { value: exchange })?;
    Ok(())
}
  • An Error and ErrorKind pair

If you choose to make your error type opaque for API concerns, you can still implement any methods you want on the opaque type, choosing very selectively what your public API is:

#[derive(Debug, Snafu)]
enum InnerError {
    MyError1 { username: String },
    MyError2 { username: String },
    MyError3 { address: String },
}

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

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum ErrorKind {
    Authorization,
    Network,
}

impl Error {
    fn kind(&self) -> ErrorKind {
        use InnerError::*;

        match self.0 {
            MyError1 { .. } | MyError2 { .. } => ErrorKind::Authorization,
            MyError3 { .. } => ErrorKind::Network,
        }
    }

    fn username(&self) -> Option<&str> {
        use InnerError::*;

        match &self.0 {
            MyError1 { username } | MyError2 { username } => Some(username),
            _ => None,
        }
    }
}

I would not call it a drop-in replacement because the API differs between the two crates. To me, "drop-in" indicates I could just change "failure" to "snafu" in Cargo.toml and everything would continue working.

SNAFU errors can use any type that implements Error as an underlying causes. In the original example, I show Box<dyn Error> as one example of that. If you are using a crate that uses Failure to build its errors, you can trivially wrap them inside of SNAFU errors. Is there any other kind of interoperation you were thinking about?

2 Likes