Skip to content

Conversation

@80Ltrumpet
Copy link

This is probably a long-shot, considering related discussions and efforts.

See in particular the doc comments for report::Compact and report::Native for usage/motivation.

@andylokandy
Copy link
Contributor

andylokandy commented Jan 18, 2026

Thanks for your contribution @80Ltrumpet. The newly added Compact and Report will allow us to fine-tune the debug and display behavior. In ScopeDB, we customize the display rendering for Exn when showing the error to the user, demonstrated in this example. However, since the default debug rendering only appears in panic report (after a buggy unwrap), we were not realizing the requirement of customizing the debug report rendering. Do you have a real world use case where customizable debug report rendering is beneficial?

@tisonkun
Copy link
Contributor

tisonkun commented Jan 18, 2026

In @Byron's PR of vendoring exn (GitoxideLabs/gitoxide#2352), there are also customized display formats like:

Code
impl<E: Error + Send + Sync + 'static> fmt::Debug for Exn<E> {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        write_frame_recursive(f, self.as_frame(), "", ErrorMode::Display, TreeMode::Linearize)
    }
}

impl fmt::Debug for Frame {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        write_frame_recursive(f, self, "", ErrorMode::Display, TreeMode::Linearize)
    }
}

#[derive(Copy, Clone)]
enum ErrorMode {
    Display,
    Debug,
}

#[derive(Copy, Clone)]
enum TreeMode {
    Linearize,
    Verbatim,
}

fn write_frame_recursive(
    f: &mut Formatter<'_>,
    frame: &Frame,
    prefix: &str,
    err_mode: ErrorMode,
    tree_mode: TreeMode,
) -> fmt::Result {
    match err_mode {
        ErrorMode::Display => fmt::Display::fmt(frame.as_error(), f),
        ErrorMode::Debug => {
            write!(f, "{:?}", frame.as_error())
        }
    }?;
    if !f.alternate() {
        write_location(f, frame)?;
    }

    let children = frame.children();
    let children_len = children.len();

    for (cidx, child) in children.iter().enumerate() {
        write!(f, "\n{prefix}|")?;
        write!(f, "\n{prefix}└─ ")?;

        let child_child_len = child.children().len();
        let may_linerarize_chain =
            matches!(tree_mode, TreeMode::Linearize) && children_len == 1 && child_child_len == 1;
        if may_linerarize_chain {
            write_frame_recursive(f, child, prefix, err_mode, tree_mode)?;
        } else if cidx < children_len - 1 {
            write_frame_recursive(f, child, &format!("{prefix}|   "), err_mode, tree_mode)?;
        } else {
            write_frame_recursive(f, child, &format!("{prefix}    "), err_mode, tree_mode)?;
        }
    }

    Ok(())
}

fn write_location(f: &mut Formatter<'_>, exn: &Frame) -> fmt::Result {
    let location = exn.location();
    write!(f, ", at {}:{}:{}", location.file(), location.line(), location.column())
}

impl<E: Error + Send + Sync + 'static> fmt::Display for Exn<E> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        std::fmt::Display::fmt(&self.frame, f)
    }
}

impl fmt::Display for Frame {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if f.alternate() {
            // Avoid printing alternate versions of the debug info, keep it in one line, also print the tree.
            write_frame_recursive(f, self, "", ErrorMode::Debug, TreeMode::Verbatim)
        } else {
            fmt::Display::fmt(self.as_error(), f)
        }
    }
}

/// A frame in the exception tree.
pub struct Frame {
    /// The error that occurred at this frame.
    error: Box<dyn Error + Send + Sync + 'static>,
    /// The source code location where this exception frame was created.
    location: &'static Location<'static>,
    /// Child exception frames that provide additional context or source errors.
    children: Vec<Frame>,
}

In error-stack, it provides a global sync hook for the user to set up a customized debug hook. When implementing exn, we intended to avoid this flavor since it would introduce a very complex ctor dependency to set up.

Another flavor is std::path::Path's display, which allows explicitly constructing a displayable struct. But Exn needs to impl (at least) Debug by default because Result::unwrap requires the Err type to implement Debug.

bytesize's Display can be another alternative, where ByteSize impl Display to use the default config of Display while users can explicitly get a Display struct and set different options.

@Byron
Copy link

Byron commented Jan 18, 2026

To my mind, using the f.alternate() flag for display and debug already helps to cover much more ground, so I'd find that (in a hopefully even better form) in exn very useful.
Lastly, I thought the customisation that exn suggests using frame iteration directly seems perfectly suitable for applications, and all I'd like to see is utilities for common traversals, like 'traverse as tree' or traverse as chain, so users can treat each frame like one line (they define) in a commonly used display style (chain, tree, etc) that the utilities help with. The full override is always possible for those who need more than that.

@80Ltrumpet
Copy link
Author

Do you have a real world use case where customizable debug report rendering is beneficial?

Yes, though it looks like better mechanics for doing this are being considered. Sorry to bother y'all.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants