This RFC proposes additions to the Error trait to support accessing generic
forms of context from dyn Error trait objects. This generalizes the pattern
used in backtrace and source. This proposal adds the method
Error::provide_context to the Error trait, which offers TypeId-based
member lookup and a new inherent function <dyn Error>::context and <dyn Error>::context_ref which makes use of an implementor's provide_context to
return a typed reference directly. These additions would primarily be useful
for error reporting, where we typically no longer have type information and
may be composing errors from many sources.
TLDR
Add this method to the Error trait
And these inherent methods on dyn Error trait objects:
Example implementation:
Example usage:
let e: &dyn Error = &concrete_error;
if let Some = e.
Motivation
In Rust, errors typically gather two forms of context when they are created:
context for the current error message and context for the final error
report. The Error trait exists to provide an interface to context intended
for error reports. This context includes the error message, the source error,
and, more recently, backtraces.
However, the current approach of promoting each form of context to a method on
the Error trait doesn't leave room for forms of context that are not commonly
used, or forms of context that are defined outside of the standard library.
Extracting non-std types from dyn Errors
By adding a generic form of these member access functions we are no longer restricted to types defined in the standard library. This opens the door to many new forms of error reporting.
Example use cases this enables
- using alternatives to
std::backtrace::Backtracesuch asbacktrace::BacktraceorSpanTrace - zig-like Error Return Traces by extracting
Locationtypes from errors gathered via#[track_caller]or similar. - error source trees instead of chains by accessing the source of an error as a slice of errors rather than as a single error, such as a set of errors caused when parsing a file TODO reword
- Help text such as suggestions or warnings attached to an error report
Guide-level explanation
Error handling in Rust consists of three main steps: creation/propagation,
handling, and reporting. The std::error::Error trait exists to bridge the
gap between creation and reporting. It does so by acting as an interface that
all error types can implement that defines how to access context intended for
error reports, such as the error message, source, or location it was created.
This allows error reporting types to handle errors in a consistent manner
when constructing reports for end users while still retaining control over
the format of the full report.
The Error trait accomplishes this by providing a set of methods for accessing
members of dyn Error trait objects. It requires that types implement the
Display trait, which acts as the interface to the main member, the error
message itself. It provides the source function for accessing dyn Error
members, which typically represent the current error's cause.
For all other forms of context relevant to an error report, the Error trait
offers the provide_context method. The report renderer indirectly calls
provide_context for any Error type that implements it using standard
library methods on dyn Error itself: <dyn Error>.request_ref and <dyn Error>.request_value.
As an example of how to use this interface to construct an error report, let’s explore how one could implement an error reporting type. In this example, our error reporting type will retrieve the source code location where each error in the chain was created (if it captured a location) and render it as part of the chain of errors. Our end goal is to get an error report that looks something like this:
Error:
0: ERROR MESSAGE
at LOCATION
1: SOURCE'S ERROR MESSAGE
at SOURCE'S LOCATION
2: SOURCE'S SOURCE'S ERROR MESSAGE
...
The first step is to define or use a type to represent a source location. In this example, we will define our own:
Next, we need to gather the location when creating our error types.
Then, we need to implement the Error trait to expose these members to the error reporter.
And, finally, we create an error reporter that prints the error and its source recursively, along with any location data that was gathered
;
Now we have an error reporter that is ready for use, a simple program using it would look like this.
Which, if run without creating the instrs.json file prints this error report:
Error:
0: Failed to read instrs from ./path/to/instrs.json
at instrs.rs:42
1: No such file or directory (os error 2)
Mission accomplished! The error trait gave us everything we needed to build
error reports enriched by context relevant to our application. This same
pattern can implement many error reporting patterns, such as including help
text, spans, http status codes, or backtraces in errors which are still
accessible after the error has been converted to a dyn Error.
Reference-level explanation
A proof of concept implementation, can be seen in Nika Layzell's dyno crate.
A usable version of this proposal is available in master branch of the rust repo.
User-facing elements
The user-facing elements of this proposal include the following (snippets copied from master branch of standard library):
Add a Request type to libcore for type-indexed context
Request is a type intended to emulate nested dynamic typing. This type fills
the same role as &dyn Any except that it supports other trait objects as the
requested type. The only users of this type outside of the standard library
will be implementors of Error.provide_context, in order to respond to
requests for specific types of data. Requests for data types not supported by
the implementor of Error.provide_context will result in None responses to
<dyn Error>.request_ref and <dyn Error>.request_value.
;
Define a generic accessor on the Error trait
A new method on trait Error. Implementors of this provide_context method
can respond to requests for specific types of data using the given &mut Request instance. Requests for data types not supported by this method on a
particular type (via <dyn Error>.request_ref or <dyn Error>.request_value)
will result in an Option::None response.
/// Provides type based access to context intended for error reports.
///
/// Used in conjunction with [`Request::provide_value`] and [`Request::provide_ref`] to extract
/// references to member variables from `dyn Error` trait objects.
Note that provide_context is not a user-facing function, and is in the
current unstable form only used indirectly through methods on dyn Error. It
is provided as a means for implementors of Error to respond to requests
routed to them via this method.
Use the Request type to handle passing generic types out of the trait object
New methods on dyn Error trait objects, intended for downstream receivers of
dyn Error instances to request data of specific types. These methods take
care of implementation details related to constructing Requests and
attempting to fulfill them via Error.provide_context.
Drawbacks
- The
Requestapi is being added purely to help with this function. This will likely need some design work to make it more generally applicable, hopefully as a struct incore::any. Update: this API might also be useful forstd::task::Contextto help pass data to executors in a backend agnostic way. - The
contextfunction name is currently widely used throughout the rust error handling ecosystem in libraries likeanyhowandsnafuas an ergonomic version ofmap_err. If we settle oncontextas the final name it will possibly break existing libraries.
Rationale and alternatives
Do Nothing
We could not do this, and continue to add accessor functions to the Error
trait whenever a new type reaches critical levels of popularity in error
reporting.
If we choose to do nothing we will continue to see hacks around the current
limitations on the Error trait such as the Fail trait, which added the
missing function access methods that didn't previously exist on the Error
trait and type erasure / unnecessary boxing of errors to enable downcasting to
extract members.
[1].
Use an alternative proposal that relies on the Any trait for downcasting
Why isn't this the primary proposal?
There are two significant issues with using the Any trait that motivate the
more complicated solution.
- You cannot return dynamically sized types as
&dyn Any - It's easy to introduce runtime errors with
&dyn Anyby either comparing to or returning the wrong type
By making all the TypeId comparison internal to the Request type it is
impossible to compare the wrong TypeIds. By encouraging explicit type
parameters when calling provide the compiler is able to catch errors where
the type passed in doesn't match the type that was expected. So while the API
for the main proposal is more complicated it should be less error prone.
Prior art
I do not know of any other languages whose error handling has similar
facilities for accessing members when reporting errors. For the most part,
prior art for this proposal comes from within rust itself in the form of
previous additions to the Error trait.
Rust RFC 3192
The now-defunct
rust-lang/rfcs#3192 proposed a
way for users outside the standard library to offer similar functionality as
with the Error.provide_context trait method being proposed here. The libs
meeting team ultimately decided to limit usage of the Request type to the
Error trait:
- May 2023 libs team meeting (summary of requested changes](https://github.com/rust-lang/rust/issues/96024#issuecomment-1554773172)
- July 9 2023 confirmation that the libs team meeting only wants this
functionality to be available for
Errortypes
Unresolved questions
What should the names of these functions be?context/context_ref/provide_context/provide_context/request_contextmember/member_refprovide/request- Update as of https://github.com/rust-lang/rust/pull/113464 we are
settling on
RequestandError'sfn provide<'a>(&'a self, demand: &mut Request<'a>)and https://github.com/rust-lang/rust/pull/112531 renamesprovidetoprovide_context
Should there be a by-value version for accessing temporaries?Update: The object provider API in this RFC has been updated to include a by-value variant for passing out owned data.We bring this up specifically for the case where you want to use this function to get anOption<&[&dyn Error]>out of an error, in this case, it is unlikely that the error behind the trait object is actually storing the errors asdyn Errors, and theres no easy way to allocate storage to store the trait objects.
Future possibilities
This opens the door to supporting Error Return Traces,
similar to zig's, where if each return location is stored in a Vec<&'static Location<'static>> a full return trace could be built up with:
let mut locations = e
.chain
.filter_map
.flat_map;
[rust-repo] https://githbu.com/rust-lang/rust