How to reduce boilerplate when wrapping errors

I am wrapping errors per this Rust By Example page: Wrapping errors - Rust By Example

Currently my code contains a lot of boilerplate:

#[derive(Debug)]
pub enum Error {
    JsonError(serde_json::Error),
    YamlError(serde_yaml::Error),
    IOError(String),
    // ...
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            Error::JsonError(ref e) => e.fmt(f),
            Error::YamlError(ref e) => e.fmt(f),
            Error::IOError(ref e) => e.fmt(f),
            // ...
        }
    }
}

impl From<serde_yaml::Error> for Error {
    fn from(err: serde_yaml::Error) -> Error {
        Error::YamlError(err)
    }
}

impl From<serde_json::Error> for Error {
    fn from(err: serde_json::Error) -> Error {
        Error::JsonError(err)
    }
}

// ...

Is there a better way to write this? It's much longer with all seven error types.

1 Like

For example GitHub - dtolnay/thiserror: derive(Error) for struct and enum error types

5 Likes

In my own code I would generally simply have a file just for the error type, but a crate such as thiserror could also work.

1 Like

I checked out thiserror, and liked some things about it, but I ultimately went with error chain. This is what my new error.rs file looks like:

use std::path::Path;
mod error {
    error_chain! {}
}

error_chain! {
    foreign_links {
        Clap(::clap::Error);
        Yaml(::serde_yaml::Error);
        Json(::serde_json::Error);
        Io(::std::io::Error);
    }
    errors {
        ScoreError(s: String) {
            display("Name not found in scores.yaml file: '{}'", s)
        }
        LcovReaderError(e: lcov::reader::Error) {
            display("{}", e)
        }
    }
}

impl From<lcov::reader::Error> for Error {
    fn from(err: lcov::reader::Error) -> Error {
        Error::from(ErrorKind::LcovReaderError(err))
    }
}

For the most part I just use ? to convert from other errors to my error type. Here's a slightly more involved example:

    let records = reader
        .collect::<std::result::Result<Vec<_>, lcov::reader::Error>>()
        .map_err(Error::from)
        .chain_err(|| {
            format!(
                "Unable to parse lcov string: 

{}",
                lcov_string
            )
        })?;

This was necessary because lcov::reader::Error happens not to implement the Error trait. Hence the extra logic in error.rs.

I went with error chain because it has good documentation, gets referenced from several tutorials, and the chain_err method is very useful for giving informative errors. For example if I introduce an unparsable error into my lcov string:

error: Unable to parse lcov string:

/*TN:*/
SF:src/solution.rs
FN:1,_ZN10assignment8solution3fib17ha0ec88ff33def664E
FN:1,_ZN10assignment8solution3fib17hbf227c473e17027fE
FNDA:1,_ZN10assignment8solution3fib17ha0ec88ff33def664E
FNDA:0,_ZN10assignment8solution3fib17hbf227c473e17027fE
FNF:2
FNH:1
BRDA:3,0,0,1
BRDA:3,0,1,1
BRDA:3,0,2,1
BRDA:5,0,0,1
BRDA:5,0,1,-
BRF:2
BRH:4
DA:1,9
DA:2,9
DA:3,9
DA:4,3
DA:5,4
DA:6,0
DA:8,9
LF:7
LH:6
end_of_record

caused by: invalid record syntax at line 1: unknown record

The part after "caused by:" is from the original error and the part after "error:" is my custom error string from chain_err.

You should probably be aware of other alternatives. Error Handling Survey provides an excellent overview of error handling in the ecosystem.

1 Like

Wow this is great. Wish I had seen this before jumping on the error-chain boat. At some point I will probably refactor with one of these more recent libraries. Thanks for the info!

Ultimately switched to snafu:

#[derive(Debug, Snafu)]
#[snafu(visibility(pub))]
pub enum MyError {
    #[snafu(display("Failed to read from {}: {}", path.display(), source))]
    ReadError { source: io::Error, path: PathBuf },

    #[snafu(display("Failed to write to {}: {}", path.display(), source))]
    WriteError { source: io::Error, path: PathBuf },

    #[snafu(display("Failed to create file {}: {}", path.display(), source))]
    FileCreationError { source: io::Error, path: PathBuf },

    #[snafu(display("Unable to parse yaml to ScoreMap:\n{}\n{}", yaml, source))]
    ScoreMapParseError {
        source: serde_yaml::Error,
        yaml: String,
    },

    #[snafu(display("Unable to serialize struct to json:\n{:?}\n{}", output, source))]
    TestOutputError {
        source: serde_json::Error,
        output: TestOutput,
    },

    #[snafu(display("Unable to serialize struct to json:\n{:?}\n{}", report, source))]
    TestReportError {
        source: serde_json::Error,
        report: TestReport,
    },

    #[snafu(display("Unable to serialize struct to json:\n{:?}\n{}", report, source))]
    ReportError {
        source: serde_json::Error,
        report: Report,
    },

    #[snafu(display("Key {} not found in ScoreMap: ", key))]
    ScoreMapKeyError { key: String },

    #[snafu(display("Bad argument {}: {}", arg, source))]
    Argument { source: clap::Error, arg: String },

    #[snafu(display("Unable to parse lcov string:\n{}", string))]
    LcovReadError { string: String },

    #[snafu(display("{}", msg))]
    AssertionError { msg: String },
}

Mostly this works very idiomatically:

serde_json::to_string_pretty(&output).context(TestOutputError { output })?

snafu is a little quirky in the case of errors that do not implement the Error trait:

let records = records.map_err(|_| {
    LcovReadError {
        string: lcov_string.clone(),
    }
    .into_error(snafu::NoneError) // Note this last unfortunate line.
})?;

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.