Why doesn't anyhow's debug formatter include the underlying debug formatting?

I have less than a month of experience with rust, so I may be missing very good reasons for this. But I was confused when actix showed me less information when logging an anyhow error than it did when logging non-anyhow errors. It appears to be using the Display trait implementation of the underlying error, not the Debug trait.

It looks like anyhow is trying to produce nice-looking, human-scannable debug output. And though I recognize it has more than a single struct to deal with, my intuition suggests that it should be possible to include the wrapped error's debug formatting in the message. Is there a reason why that's not sensible to do, or is this something that might be added?

I think it's reasonably assuming that Debug never includes more information than Display - it often has more accurate / exact field information, but a good Display implementation, especially on an error type, should always include all applicable information.

If there's an error type which is missing information from its Display implementation, I'd file that as bug in the crate which exposes that error type.

anyhow::Error is a pretty much wrapper around dyn std::error::Error. The std::error::Error trait requires Display, but not Debug.

3 Likes

That would indeed be a very good reason why it can't use Debug. Thanks!

No, std::error::Error requires both Debug and Display.

2 Likes

I believe the relationship between these traits is exactly the opposite: Debug needs to expose all the information about internal state, Display can be lossy. For example, Path's Display is lossy:

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=60c2ab2ef0c754575cc7d6ff73e89268

Another lossy display is for proc_macro::Ident (it erases hygiene info).

The contract of Debug is that it is shown to the developer, and should be lossless if possible.
Display is generally what is shown to the end user, and it's ok for it to be lossy.

The reason why anyhow works this way is very specific and technical.

The

fn main() -> anyhow::Result<()> {
    ...
}

syntax would use fmt::Debug to print the error message on exit. I think there's a rough consensus today that this is a small bug in this feature, and that it really should have been implemented with fmt::Display instead (because the result is for the end-user consumption).

Anyhow is designed to work well with such syntax, so it uses a manual Display-like implementation of Debug, and not the more conventional #[derive].

3 Likes

That bug is a feature! It's intentionally ugly. It was meant to discourage people from using it for anything than throw-away examples.

When the Terminate trait was discussed, the thinking was that applications which would use it for real errors displayed to users would like to customize the output, and one-size-fits all default display wouldn't be satisfactory to anyone. But display customization by implementing traits or type wrappers or magic attributes just to change few lines of text seemed absurd complexity in comparison to using if let Err(e) in main() instead.

So Debug is there for people to use this only in examples and throw-away code, and put code in main for real custom output.

anyhow outsmarted the system :slight_smile: If error in main used Display, anyhow couldn't use it! That's because it's typical for other errors to wrap their parent in something like operation failed: {}, and that would grow messages exponentially if they included their error chains in Display.

1 Like

I strongly disagree. Debug output isn't what end users of your applications should be seeing. So I don't think examples should be encouraging that. That's why if an example includes main, I still write it like this:

fn main() {
    if let Err(err) = try_main() {
        eprintln!("{}", err);
        std::process::exit(1);
    }
}

fn try_main() -> Result<(), Box<dyn std::error::Error>> {
    // the example...
}

If main() -> Result had used Display formatting, then I wouldn't bother with the try_main at all.

To be honest I don't think the Display trait is good for end user display unless your application only provides very limited feedback. E.g. the application only supports one language and is just naively passing on messages generated from the OS or elsewhere.

That said, a lot of unix-like CLI applications are well suited to Display because they require a rigid output that can be (more or less) reliably parsed by grep and other string parsing tools.

I agree with you that try_main is the best practice. By examples I've meant cases where main's error handling isn't the point of the example, and it needs to get out of the way of other code being shown. It's not ideal, but it's meant to just make short code snippets compile with ? instead of even worse .unwrap() everywhere.

I understand. I just don't agree. I try not to use unwrap or rely on Debug impls in main, which is why I use try_main.

I'm not making an argument that Display is the final word on all user visible error messages. I'm only making an argument that Display is better than Debug, and errors returned by main should default to a display that is better for end users. For many applications, like UNIX cli tools (a niche where Rust shines), this is all you need. Thankfully anyhow is mitigating this somewhat, but otherwise, the default main -> Result handling ends up being the worse among two choices for end users.

And for that exact reason it's considered bad practice to do error Display implementations like this.

When implementing an error type, you should either

  • Have Display be for this error in the chain only, and set Error::source to the wrapped error (if any), or
  • Have Display be for this error and the directly wrapped error (roughly a "context" error), and set Error::source to the wrapped error's Error::source.

The intent is that printing each error in the source chain does not then duplicate information. The reason so many errors don't do this currently is that it's a lot more convenient to do panic!("{}", err) than to print out each error in the source chain.

Then you wrap this up in an "error reporting" type such as anyhow::Error or eyre::Report which does all of the formatting required. (I'd suggest clicking through to the eyre::Report page: it does some clever things with display formatting that I think are really cool. There are four display formats available: {:}, {:#}, {:?}, and {:#?}, and eyre shows something different for each.)

I'm excited for @yaahc's talk at RustConf, which should hopefully help clarify this for a lot of people.


And I'd like to add that even if main() -> Result<_, _> used Display, "error reporting"/"application error" types would still likely override Debug to use Display-like "nice" formatting, as it's useful to get the "nice" errors shown on .unwrap() as well.

1 Like

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.