A new error_fmt method on std::error::Error,
so that we can distinguish:
- Requests to just display an error for human consumption
(
Display) - The internal implementation of printing a particular error,
excluding its
sources (error_fmt).
Transitional and compatibility arrangements to make this workable.
Motivation
[motivation]:
Correctly printing errors in Rust (and defining errors that print correctly) is too hard.
We want to be able to get from where we are now to a situation with the following properties:
- Just printing an error with
eprintln!("{error}")will reliably do something useful. - Errors can be printed in a fancy report-like style with inspection of source errors, if desired.
- Messages, and parts of them, are not duplicated.
- Implementing an error type isn't significantly harder than today.
- Warts (induced by backward compatibility requirements) are avoided as much as possible.
Guide-level explanation (synchronic - where we want to end up)
Background (existing situation, will not be changed by this RFC)
Most errors should implement std::error::Error.
Errors can have a "source": an underlying error which caused this one. That underlying error can in turn have a source, forming a causal chain.
Printing errors (new doctrine)
Errors can be printed in two main ways:
Every error implements Display
and provides an error_fmt method.
The Display implementation does print the source.
and should be used whenever an error (possibly and its causes)
needs to be printed for human consumption or logging.
The error_fmt method does not print the source of an error.
It is called to print the details of this error.
Normally, an implementor of an error will
provide an implementation of error_fmt.
There are macro packages in the crate ecosystem to help with this.
An implementor of an error type will usually
rely on a standard library default implementation
of Display.
Reference-level explanation
/// Displays `E` and all its sources; output is similar to "error: source".
///
/// When used with `{:#}`, prints a multi-line "caused by" chain.
///
/// Does ad-hoc deduplication, as follows: Records the string of the
/// each error displayed, and suppresses printing of the source if the
/// source error text is textually contained within the previous error
/// text.
default
If neither error_fmt, nor an explicit Display impl,
is provided, a deny-by-default lint
(or perhaps a compilation failure) is triggered.
Technical background
Rust is confused about how to print errors.
The key question is "should Display print the source"?
There is no good answer.
"Yes" implies that every error is responsible for its own formatting, and can result in duplicated output.
"No" means that the Display implementation is a footgun:
if you just print an error in the most obvious way,
your program will print vacuous errors in the common case
where libraries wrap up errors from lower libraries.
This question has been considered by the Error Handling Working Group. Their recommendation is that the answer should be "no".
This RFC proposes an alternative to that decision. Principally, because experience shows that the "vacuous error messages" problem can be quite pervasive and severe. By their nature, error paths are less well-tested, so it is important that the obvious way of error handling is correct (or that tooling will catch mistakes).
Analysis
The problem stems from the fact that there are necessarily two error printing concepts:
-
Reporting a whole error including its sources,
-
Printing only this error
Here, (2) forms part of the implementation of (1).
The operation (1) of printing a whole error chain
can be done in terms of the source() method and
(2) printing individual errors.
The question is: what should these two APIs be called and where should they live?
The EHWG recommendation answers this as:
(1) should be provided by a separate reporting function,
such as a (not yet existing) stdlib facility,
or crates like anyhow and eyre.
(2) should be provided through the Display impl.
But this approach is is wrong:
the "usual" way of printing an error should be (1),
and that is what the Display impl ought to mean
(since that is what Display is for).
In this RFC we answer these questions as follows:
-
Reporting a whole error is done by
Displaying it, or by using a special library if you want more control. -
The implementation API for "print just this error" is a new trait method
Error::error_fmt.
The remainder of the RFC follows from this decision, and from the need to maintain backwards compatibility.
Transition plan
-
Introduce the new
error_fmtmethod and defaultDisplayimpl (including necessary language/compiler features). -
Packages whose MSRV is new enough implement
error_fmtinstead ofDisplay. -
For example, macro packages like
thiserrorrelease a major version:- newer MSRV
- implement
error_fmt(as per 2.) - fail to compile if a provided format error string includes the error's source.
-
In the 2024 edition, issue a warning for use of the provided
error_fmt(ie, for non-implementation oferror_fmt).
Drawbacks
-
This is reversing a recommendation by the Error Handling Working Group. (This recommendation is not, however, present in the stdlib documentation.)
-
Almost every implementor of
Errorwill need to change eventually. (But this is often done with macro packages.) -
The ad-hoc deduplication in the default
Displayimpl is rather unprincipled, and involves rather too much boxing. (However, it is simple and effective.) -
This exposes the use of specialisation in the stdlib API.
-
This introduces the use of
#[feature(specialization)]tocorerather than justmin_specialization. Moreover, the proposed blanket impl does not compile with current Rust. Compiler work would be needed. -
Additionally, compiler work may be needed to provide the lint for failure to manually implement either
Displayorerror_fmt. -
Codebases that wish to avoid using the default error formatting, and always want to use a custom reporter, will need to somehow find a way to lint for that. This is not a thing that clippy can currently do.
Alternatives
Firm up EHWG recommendation to not include source in Display
If that recommendation were followed by all types implementing Error,
and all programs that wanted to print errors
didn't just use Display,
but some reporting facility that does print sources,
then programs would have correct behaviour overall.
Achieving this, and maintaining that state, is not trivial. It would probably involve:
-
A new lint when an
Error'sDisplayis used, butError::sourceisn't called "nearby". This new lint is necessary to catch the easy mistake of printing an error without its source; experience shows that this mistake can be ubiquitous in codebases that adopt the EHWG recommendation. -
A convenient new facility in the stdlib for printing errors. For example, a new provided method on
Errorthat returns something that isDisplayand which prints the error and all its sources.
A downside of this approach is that the Display
impl for every error is forever "wrong":
normally, Display prints a thing in the most usual way,
but for errors, Display is part of the implementation,
and actual printing must be done with some kind of reporter.
Marker trait or macro for implementing Display
Instead of default impl Display for Error,
we could have a library function for use in Display impls,
and a macro that implements Display in terms of it.
But macro calls have a much less obvious meaning to the reader of the code.
Alternatively, there could be a marker trait:
/// Displays `E` and all its sources; output is similar to "error: source".
///
/// When used with `{:#}`, prints a multi-line "caused by" chain.
/// Does ad-hoc deduplication.
But the blanket
impl<E> Display for E where E: Error + ErrorDisplay
is rejected by the current compiler,
because of a conflict with the blanket
Display impls for references, Pin etc,
if user crates impl ErrorDisplay for &....
This would still need to be dealt with by specialisation.
Declare a difference between {:#} and {}
We could say that whether to include sources should depend on
fmt::alternate(), which comes from the # in {:#}.
However:
-
Conceptually, this is wrong. The two kinds of output are not different styles of display of the same information; indeed, they aren't really sensibly used by the same callers. Sources should always be included in errors shown to the user. When omitting them is required, it is not because they are clutter, but because somewhere else in the reporting machinery is printing them.
-
Formatting without the source is needed only by error reporting/formatting machineries, of which there are going to be relatively few (and their authors will be error display experts). Conversely, most programmers must frequently write code to display of errors to the human user, and in that case the sources should be included. That suggests
{}should include the source and{:#}should exclude it. But usually the output from{:#}is longer, whereas here it would be shorter. Andeyre::Reporthas the opposite convention. -
The two kinds of display want to be implemented in different places: we want to provide a default implementation of the user-visible display including sources; conversely, we want errors to define the display of their own content. But
fmt::alternate()isn't sensible to use for dispatch. -
{:#}vs{}has a better potential meaning for errors: do we display everything on a single line, or in multi-line "caused by" format.
Replace the Error trait completely
This would be a very big job and probably highly disruptive.
It might involve inventing a new mechanism for allowing evolution of stdlib traits across editions, or something.
Do nothing
We could let the ecosystem blunder on, perpetrating programs that produce vacuous or duplicated error messages.
Prior art
The problems with the Error trait are specific to Rust.
The EHWG recommends not to print error sources as part of Display.
anyhow::Error etc. don't implement std::error::Error.
They do implement a useful Display
which includes all error sources.
eyre::Report provides a way to define the way errors are reported.
Like anyhow::Error, it doesn't implement std::error::Error.
eyre::Report includes error sources when printed with {:#}
and not when printed with {}.
snafu::CleanedErrorText
implements textual error message deduplication
which is similar in spirit to that proposed in this RFC.
Arti's codebase follows the EHWG recommendation, and has tools for use in error display contexts (such as logging).
Unresolved questions
-
What should
error_fmtbe called. -
What about
no_std? The proposed ad-hoc duplication can't sensibly be done without all cation. -
What about localisation and message translation? Are future efforts in that area going to render this all moot?
-
Should there be a way for someone who has an
Errorto tell iferror_fmtwas defaulted to "useDisplay" ? Without this, we might never be able to get rid of the extra string formatting and allocations.
Future possibilities
Hopefully this will be the last churn in this area.
The default error reporter with string deduplication
could use some magic to discover whether the provided
error_fmt-in-terms-of-Display was being used by a particular error.
If it isn't it knows it won't need to deduplicate it;
it then doesn't need to format to a string.
So the old efficiency is regained.