snafu icon indicating copy to clipboard operation
snafu copied to clipboard

Document how to use errors that implement failure's `Fail` as an underlying source

Open MattesWhite opened this issue 5 years ago • 3 comments

I just started using snafu and up to now it is much more egonoic for me than failure. Therefore, thanks for this crate!

However, working with snafu I stumbled on a problem today. I just can't figure out how to handle Fails that I get from an external crate:

use snafu::{Snafu, ResultExt};
use rumqtt::{MqttClient, MqttOptions};
use std::fmt;

#[derive(Debug, Snafu)]
enum Error {
    /// source requires to impl `Error` which the underlying `Fail` typically not does.
    /// Therefore, I use my own Error type that holds a `String`.
    #[snafu(source(from(rumqtt::ConnectError, TextError::from)))]
    #[snafu(display("Failed to connect to MQTT broker: {}", source))]
    Connection { source: TextError },
}

#[derive(Debug)]
struct TextError(String);

impl std::error::Error for TextError {}

impl fmt::Display for TextError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl TextError {
    fn from<T: fmt::Display>(d: T) -> Self {
        Self(d.to_string())
    }
}

/// However this does not work
fn main() {
    let ops = MqttOptions::new("client", "localhost", 1883);
    let (mut client, notifier) = MqttClient::start(ops).context(Connection).unwrap();
}

The compiler states that Connection requires a TextError instead of an rumqtt::ConnectError. Probably, I did something wrong or the #[snafu(source(from(...))) does not what I think it does.

Any advice how to handle this (without .map_err())?

PS: I give a talk about error handling in Rust at my local Rust Meetup group this evening where I want to present snafu , so a quick answer would be much appreciated.

MattesWhite avatar Aug 07 '19 10:08 MattesWhite

The issue here is that #[snafu(source(from(...)))] is in the wrong location; it needs to be on the source field:

Incorrect

#[derive(Debug, Snafu)]
enum Error {
    #[snafu(source(from(rumqtt::ConnectError, TextError::from)))]
    #[snafu(display("Failed to connect to MQTT broker: {}", source))]
    Connection { source: TextError },
}

Correct

#[derive(Debug, Snafu)]
enum Error {
    #[snafu(display("Failed to connect to MQTT broker: {}", source))]
    Connection { 
        #[snafu(source(from(rumqtt::ConnectError, TextError::from)))]
        source: TextError,
    },
}

The good news is that version 0.5 of SNAFU will have much better error reporting about misused attributes (#105), so this would have been more obvious.


That being said, I'd suggest using failure's Compat struct instead of creating your own:

use snafu::{Snafu, ResultExt};
use rumqtt::{MqttClient, MqttOptions};

#[derive(Debug, Snafu)]
enum Error {
    #[snafu(display("Failed to connect to MQTT broker: {}", source))]
    Connection {
        #[snafu(source(from(rumqtt::ConnectError, failure::Fail::compat)))]
        source: failure::Compat<rumqtt::ConnectError>
    },
}

fn main() {
    let ops = MqttOptions::new("client", "localhost", 1883);
    let (mut client, notifier) = MqttClient::start(ops).context(Connection).unwrap();
}

shepmaster avatar Aug 07 '19 13:08 shepmaster

Opinion wise, this is an aspect of failure that I dislike — the fact that an error is implemented with failure "leaks out" in the public API of the crate, making interoperation that much harder.

shepmaster avatar Aug 07 '19 13:08 shepmaster

Thank you, I was pretty sure that I simply placed the attribute false. Now it works 👍

MattesWhite avatar Aug 07 '19 13:08 MattesWhite