Skip to content

Conflicting ergonomics for source chains #214

Closed

Description

@Ralith

For errors defined by context information and an inner source, we usually want to display both the context and the source to the user. thiserror makes this convenient through annotations like #[error("{context}: {source}")]. However, this pattern will lead to ugly and confusing duplication of information when combined with anyhow's handy feature to format source chains automatically. On the other hand, using something like #[error("{context}")] leads to worse behavior for users who invoke the error's Display impl directly rather than going through anyhow.

anyhow is very popular, but it is also reasonable to expect crucial details to be Displayable without requiring custom traversal code. How can we resolve this conflict?

One idea is for thiserror itself to provide a convenient interface for traversing source chains. Maybe this could be an inherent method, or exposed through the alternate Display mode ({:#}) as done by anyhow, giving consistent Display behavior across both crates.

Activity

  1. Ralith commented on Dec 22, 2022

    @Ralith
    Author

    I attempted implementing {:#} support by modifying the generated Display::fmt body to be

                        let result: std::fmt::Result = { #body };
                        if let e@Err(_) = result {
                            return e;
                        }
                        if __formatter.alternate() {
                            let mut next = <Self as std::error::Error>::source(self);
                            while let Some(source) = next {
                                write!(__formatter, ": {}", source)?;
                                next = source.source();
                            }
                        }
                        Ok(())

    but that fails for this test case:

    #[derive(Error, Debug)]
    #[error(transparent)]
    pub struct StructTransparentGeneric<E>(E);

    because Self isn't necessarily Error. Adding Self: Error to the where clause of the Display impl seems to cause additional problems. Maybe this could be mitigated by inlining a copy of the generated source method (if any). Thoughts?

  2. MarijnS95 commented on Dec 23, 2022

    @MarijnS95

    This is the main reason why I've started returning anyhow out of fn main() instead of a custom thiserror structure: Rust invokes the Debug (not Display) implementation which intentionally prints a useful source chain "backtrace": https://docs.rs/anyhow/latest/anyhow/struct.Error.html#display-representations.

    For this to make sense though I've also had to clean up stray #[error("{context}: {source}")] for the ugly confusing duplication reason you mentioned; meaning source context is lost in the event that .unwrap() gets called on a thiserror (Which also calls Debug).

    So, while at it, is it possible/reasonable to look into providing a similar Debug implementation to anyhow in addition to this alternate Display format?

    https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=232a009a895d8d41d18d26e0056a55b9

  3. MarijnS95 commented on Dec 23, 2022

    @MarijnS95

    Looks like there are more issues open on the topic of merge ergonomic/consistent source chain formatting (especially when considering thiserror and anyhow being complementary):

  4. dtolnay commented on Feb 17, 2023

    @dtolnay
    Owner

    Closing in favor of #98.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions