libs (traits | error-handling)
fix_error
Change the std::error::Error
trait to improve its usability. Introduce a
backtrace module to the standard library to provide a standard interface for
backtraces.
The Error
trait has long been known to be flawed in several respects. In
particular:
description
method is limited, usually, to returning static
strings, and has little utility that isn't adequately served by the required
Display
impl for the error type.cause
method does not allow the user to downcast the
cause type, limiting the utility of that method unnecessarily.We propose to fix this by deprecating the existing methods of Error
and adding
two new, provided methods. As a result, the undeprecated portion of the Error
trait would look like this:
trait Error: Display + Debug {
fn backtrace(&self) -> Option<&Backtrace> {
None
}
fn source(&self) -> Option<&dyn Error + 'static> {
None
}
}
The new API provides three main components:
source
method. This returns another Error type, which is the underlying
source of this error. If this error has no underlying source (that is, it is
the "root source" of the error), this method should return none.This RFC adds a new backtrace
module to std, with one type, with this API:
pub struct Backtrace {
// ...
}
impl Backtrace {
// Capture the backtrace for the current stack if it is supported on this
// platform.
//
// This will respect backtrace controlling environment variables.
pub fn capture() -> Backtrace {
// ...
}
// Capture the backtrace for the current stack if it is supported on this
// platform.
//
// This will ignore backtrace controlling environment variables.
pub fn force_capture() -> Backtrace {
// ...
}
pub fn status(&self) -> BacktraceStatus {
// ...
}
}
impl Display for Backtrace {
// ...
}
impl Debug for Backtrace {
// ...
}
#[non_exhaustive]
pub enum BacktraceStatus {
Unsupported,
Disabled,
Captured
}
This minimal initial API is just intended for printing backtraces for end users. In time, this may grow the ability to visit individual frames of the backtrace.
Today, the RUST_BACKTRACE
controls backtraces generated by panics. After this
RFC, it also controls backtraces generated in the standard library: no backtrace
will be generated when calling Backtrace::capture
unless this variable is set.
On the other hand, Backtrace::force_capture
will ignore this variable.
Two additional variables will be added: RUST_PANIC_BACKTRACE
and
RUST_LIB_BACKTRACE
: these will independently override the behavior of
RUST_BACKTRACE
for backtraces generated for panics and from the std API.
Deprecating both cause
and description
is a backward compatible change, and
adding provided methods backtrace
and source
is also backward compatible.
We can make these changes unceremoniously, and the Error
trait will be much
more functional.
We also change the default definition of cause
, even though it is deprecated:
fn cause(&self) -> Option<&dyn Error> {
self.source()
}
This way, if an error defines source
, someone calling the deprecated cause
API will still get the correct cause type, even though they can't downcast it.
The addition of source
and the deprecation of cause
will be instantly
stabilized after implementing this RFC.
The addition of the backtrace method and the entire backtrace API will be left
unstable under the backtrace
feature for now.
cause
-> source
The problem with the existing cause
API is that the error it returns is not
'static
. This means it is not possible to downcast the error trait object,
because downcasting can only be done on 'static
trait objects (for soundness
reasons).
The behavior of backtraces is somewhat platform specific, and on certain platforms backtraces may contain strange and inaccurate information. The backtraces provided by the standard library are designed for user display purposes only, and not guaranteed to provide a perfect representation of the program state, depending on the capabilities of the platform.
The failure crate defines a Fail
trait with an API similar to (but not
exactly like) the API proposed here. In a breaking change to failure, we would
change that trait to be an extension trait to Error
:
// Maybe rename to ErrorExt?
trait Fail: Error + Send + Sync + 'static {
// various provided helper methods
}
impl<E: Error + Send + Sync + 'static> Fail for E {
}
Instead of providing a derive for Fail, failure would provide a derive for the std library Error trait, e.g.:
#[derive(Debug, Display, Error)]
#[display = "My display message."]
struct MyError {
#[error(source)]
underlying: io::Error,
backtrace: Backtrace,
}
The exact nature of the new failure API would be determined by the maintainers of failure, it would not be proscribed by this RFC. This section is just to demonstrate that failure could still work using the std Error trait.
This causes some churn, as users who are already using one of the deprecated methods will be encouraged (by warnings) to change their code, and library authors will need to revisit whether they should override one of the new methods.
The most obvious alternative to this RFC would be to provide an entirely new
error trait. This could make deeper changes to the API of the trait than we are
making here. For example, we could take an approach like failure
has, and
impose stricter bounds on all implementations of the trait:
trait Fail: Display + Debug + Send + Sync + 'static {
fn cause(&self) -> Option<&dyn Fail> {
None
}
fn backtrace(&self) -> Option<&Backtrace> {
None
}
}
Doing this would allow us to assemble a more perfect error trait, rather than limiting us to the changes we can make backwards compatibly to the existing trait.
However, it would be much more disruptive to the ecosystem than changing the
existing trait. We've already seen some friction with failure and other APIs
(like serde's) that expect to receive something that implements Error
. Right
now, we reason that the churn is not worth slight improvements that wouldn't be
compatible with the Error trait as it exists.
In the future, if these changes are not enough to resolve the warts with the Error trait, we could follow this alternative: we would deprecate the Error trait and introduce a new trait then. That is, accepting this RFC now does not foreclose on this alternative later.
source
The replacement for cause
could have another name. The only one the RFC author
came up with is origin
.
This proposal is largely informed by our experience with the existing Error
trait API, and from the experience of the failure
experiment. The main
conclusions we drew:
Fail
trait in failure has a better API.Error
and Fail
)
can be very disruptive.This RFC threads the needle between the problem with the Error trait and the problems caused by defining a new trait.
This RFC intentionally proposes a most minimal API. There are a number of API extensions we could consider in the future. Prominent examples:
Display
and Error
in the standard
library.Error
that have been experimented with in
failure, such as the causes iterator.None of these are proposed as a part of this RFC, and would have a future RFC discussion.
Additionally, the choice to implement nullability internal to backtrace may
prove to be a mistake: during the period when backtrace APIs are only available
on nightly, we will gain more experience and possible change backtrace's
constructors to return an Option<Backtrace>
instead.