rust-lang / rust-lang/rust

Recommendation for reporting cause either in `Error::source` or `Display` is problematic

Open
#161,303 4 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

A-docs T-libs
Dominant language
Rust
Stars
119k
Forks
16.1k
PR merge metrics
PR metrics pending

Description

Location (URL)

https://doc.rust-lang.org/std/error/trait.Error.html#error-source

Summary
Problem

The Error documentation says:

In error types that wrap an underlying error, the underlying error should be either returned by the outer error’s Error::source(), or rendered by the outer error’s Display implementation, but not both.

(see also #145561 and #146679; note that this exact wording only evolved as part of PR review)

This recommendation of using either Error::source or Display to report a cause might be problematic, because it assumes that all errors in the call chain consistently use the same approach. This is unrealistic, especially when using the standard library or external crates. It is quite likely that some error impls use one approach and some the other.

While that recommendation avoids duplicate information, it now risks that instead information is not shown at all (which in my opinion is more problematic).

It might be better to always report the cause as source(), and optionally include it in Display.
(The only exception might be if you know for sure that the cause itself cannot have any nested cause.)

Example

Consider the following example. LibError2 uses the source() approach, whereas AppError uses the Display approach.

fn print_error(e: impl Error) {
    println!("{e}");

    let mut maybe_cause = e.source();
    while let Some(cause) = maybe_cause {
        println!(" caused by: {cause}");
        maybe_cause = cause.source();
    }
}

#[derive(Debug)]
struct LibError1;
impl Error for LibError1 {}
impl Display for LibError1 {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "LibError1")
    }
}

#[derive(Debug)]
struct LibError2(LibError1);
impl Error for LibError2 {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        Some(&self.0)
    }
}
impl Display for LibError2 {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "LibError2")
    }
}

#[derive(Debug)]
struct AppError(LibError2);
impl Error for AppError {}
impl Display for AppError {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "AppError: {}", self.0)
    }
}

let e = AppError(LibError2(LibError1));
print_error(e);

Notice how the printed error is just "AppError: LibError2", without the inner LibError1 being mentioned at all.

Potential solution

Reword the documentation to say that in most cases the cause should be reported as source(), and it might want to omit it from Display to avoid error reporting showing duplicate information.

But:

  • don't use 'should'; there might still be value in duplication if error reporting just shows to_string() without inspecting source()?
  • don't imply the opposite as well (which was the case previously with 'either'); that is, if Display includes the cause, the user should not omit it from source()

(Disclaimer: I don't have that much experience with error handling libraries in Rust, so maybe the current recommendation does make sense. But to me it feels problematic from troubleshooting perspective; if you have to choose, then I think it is better to have duplicated information than missing crucial information.)

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start at the Error::source documentation section linked in the issue, then review the related issues #145561 and #146679 for the wording’s history. Evaluate the source-versus-Display recommendation against the provided mixed-implementation example. Done means the documentation clearly preserves access to nested causes while explaining when Display may include them.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
documentation
Issue type
Documentation
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.