Search code examples
rusterror-handlingtype-traits

Why does a Rust custom error enum require Display implemented when I have to format the output anyway?


I have a custom error enum that wraps a couple of common errors in my code:

pub enum ParseError {
    Io(io::Error),
    Parse(serde_json::error::Error),
    FileNotFound(PathBuf, io::Error),
}

This enum implements the Display trait (as is required):

impl Display for ParseError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ParseError::Io(io_error) => write!(f, "{}", io_error),
            ParseError::Parse(parse_error) => write!(f, "{}", parse_error),
            ParseError::FileNotFound(file, err) => {
                write!(f, "Could not open file {}: {}", file.to_string_lossy(), err)
            }
        }
    }
}

Note that for the "custom error" FileNotFound, I need to write! both the error and the filename that comes with it. However, later when I handle this error, I am required to print both the filename and the error again:

    match my_function(arg1, arg2) {
        Ok(_) => (),
        Err(error) => match error {
            Io(err) => //do stuff,
            },
            Parse(err) => //do stuff
            },
            FileNotFound(file, err) => {
                println!("Can't find file '{}': {}", file.to_string_lossy(), err)
            }

Without formatting the filename and the error in the Match, Rust just prints the generic error (in this case "The system cannot find the file specified. (os error 2)".

My question is: why is it required to first implement Display and format the error, if it is required to format it again in order to print/use it?


Solution

  • It's not required to format it again. This particularly useful when you aren't using match on it, but when you do, you can use @ to create a binding to the whole item:

    match my_function(arg1, arg2) {
        Ok(_) => (),
        Err(error) => match error {
            Io(err) => { //do stuff,
            }
            Parse(err) => { //do stuff
            }
            error @ FileNotFound(..) => {
                println!("{error}");
            }
        },
    }
    

    You could also construct the error in-place:

    FileNotFound(file, err) => {
        println!("{}", FileNotFound(file, err));
    }
    

    Another common thing is to use a catch-all to perform an action on all unmatched errors:

    error => {
        println!("{error}");
    }