How to use std::error::Error, Display, and Error::source correctly, and how to report errors

I sometimes wrap errors (to provide an abstraction from lower-level library errors).

There have been some discussions on what's the correct way to implement std::fmt::Display for std::error::Errors which have a source: Do you include a description of the underlying error, or should that be done by the error reporting mechanism?

This is what I would do now:

use std::error::Error;
use std::fmt;

#[derive(Debug)]
pub struct InnerError {
    description: String,
}

impl fmt::Display for InnerError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.description)
    }
}

impl Error for InnerError{}

#[derive(Debug)]
enum OuterErrorVariant {
    A(InnerError),
    B(InnerError),
}

#[derive(Debug)]
pub struct OuterError {
    variant: OuterErrorVariant,
}

impl Error for OuterError{
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match &self.variant {
            OuterErrorVariant::A(inner) => Some(inner),
            OuterErrorVariant::B(inner) => Some(inner),
        }
    }
}

impl fmt::Display for OuterError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self.variant {
            OuterErrorVariant::A(_) => write!(f, "error A"),
            OuterErrorVariant::B(_) => write!(f, "error B"),
        }
    }
}

fn run() -> Result<(), Box<dyn Error>> {
    let result: Result<(), OuterError> = Err(OuterError {
        variant: OuterErrorVariant::A(
            InnerError { description: "internal problem".to_string() },
        ),
    });
    result?;
    Ok(())
}

fn main() {
    match run() {
        Ok(()) => (),
        Err(err) => {
            println!("Error: {err}");
            let mut outer = &*err;
            while let Some(source) = outer.source() {
                println!("Cause: {source}");
                outer = source;
            }
        }
    }
}

(Playground)

Output:

Error: error A
Cause: internal problem

Is that the best way to go currently?

Further links on the issue:

I just realized that {:#} is a perfectly valid format specifier – Display can have an alternate form too. Not used by std impls, and probably not commonly used anywhere else either, but it could be used to signify a "verbose" or "detailed" format, for instance.

2 Likes

So you would propose {} returns only the outer message while {:#} returns the whole chain? With or without newlines?

I'd probably go with newlines, in vague analogy to {:#?}.

1 Like

That would certainly make error reporting easier (using std).

From e.g. shepmaster/snafu#363, I had understood that those discussions had resolved in favor of not including the source information in Display output. I see now I seem to have understood wrongly.

My personal, opinionated answer would be to use snafu and let it choose which way. I suppose a more popular answer would be to use something of dtolnay's and let it choose.