Why does `Error` require `Display`, but then not use it?

A question I haven't been able to wrap my head around:
Why does [std::error::Error] require [std::fmt::Display], but then seems to use [std::fmt::Debug] instead?

For example, running this:

/// Empty struct, used as an Error-type
struct MyError;

impl std::fmt::Debug for MyError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "the `Debug` message")
    }
}

impl std::fmt::Display for MyError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "the `Display` message")
    }
}

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

fn main() -> Result<(), MyError> {
    Err(MyError)
}

(Playground)

Produces:

   Compiling playground v0.0.1 (/playground)
    Finished dev [unoptimized + debuginfo] target(s) in 0.98s
     Running `target/debug/playground`
Error: the `Debug` message

I suppose by "use", I mean "use when printing error messages" - which I imagine is the point of Error requiring Display in the first place.

What's the idea here?

The main function is bound not by Error, but by Termination (note the blanket implementations). As you can see, it's based around Debug.

I have seen some remorse that it was not based around Display, and I agree -- user-friendly messages still require wrapping main, a pattern that Termination aimed to obsolete (but failed IMO).

Error types themselves don't "use" either Display or Debug. The Display bound on errors is an additional guarantee towards the eventual consumer of errors, which says "all errors must be displayable", and therefore whoever actually ends up getting an error can print it.

This has nothing to do with Result-in-main; there are a lot more places where you can handle and print an error. It happens to be the case that main doesn't use Display. But whether a trait is used internally is not necessarily the point of adding a trait bound. In this case, it's much more about adding a guarantee to all types often used in public APIs.

10 Likes

Result returned from main is meant for short examples and quick prototyping, and not for quality programs shipped to users. If you want errors to print a nice message, use:

fn main() {
   if let Err(e) = run() {
      eprintln!("Nice error info here: {}", e);
      std::process::exit(1);
   }
}
4 Likes

If you use anyhow in your main application, then you’ll get the Display message, too. (They achieve this by using the Error’s Display implementation for any encapsulated dyn Error, even for the Debug implementation of anyhow::Error; this is done precisely for supporting the use-case of using it as return type from main.)

/// Empty struct, used as an Error-type
struct MyError;

impl std::fmt::Debug for MyError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "the `Debug` message")
    }
}

impl std::fmt::Display for MyError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "the `Display` message")
    }
}

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

fn main() -> anyhow::Result<()> {
    Err(MyError)?
}
compiling playground v0.0.1 (/playground)
    Finished dev [unoptimized + debuginfo] target(s) in 1.16s
     Running `target/debug/playground`
Error: the `Display` message

(playground)

2 Likes

Very thoughtful replies thanks.

I take it the #[test] macro also uses the same Termination trait-bound then. Replacing main with a #[test] like so:

/// Empty struct, used as an Error-type
struct MyError;

impl std::fmt::Debug for MyError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "the `Debug` message")
    }
}

impl std::fmt::Display for MyError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "the `Display` message")
    }
}

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

#[test]
fn test_me() -> Result<(), MyError> {
    Err(MyError)
}

Produces:

running 1 test
test test_me ... FAILED

failures:

---- test_me stdout ----
Error: the `Debug` message   // <= (Author-added: here) 
thread 'test_me' panicked at 'assertion failed: `(left == right)`
  left: `1`,
 right: `0`: the test returned a termination value with a non-zero status code (1) which indicates a failure', /rustc/c8dfcfe046a7680554bf4eb612bad840e7631c4b/library/test/src/lib.rs:194:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    test_me

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
1 Like

Yes. Which is probably what you want for a test (vs. user-facing errors).

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.