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. Via
#![feature(backtrace)] it provides the backtrace function, for accessing a
Backtrace of the state of the stack when an error was created. For all other
forms of context relevant to an error report, the Error trait provides the
context, context_ref, and provide_context` functions.
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 be 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
The following changes need to be made to implement this proposal:
Add a Request type to libcore for type-indexed context
Request is a type 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.
Here is the implementation for the proof of concept, based on Nika Layzell's dyno crate:
A usable version of this is available in the proof of concept repo under
fakecore/src/any.rs.
use TypeId;
extern crate alloc;
use Box;
/// An identifier which may be used to tag a specific
/// Sealed trait representing a type-erased tagged object.
pub unsafe
/// Internal wrapper type with the same representation as a known external type.
unsafe
// FIXME: This should also handle the cases for `dyn Tagged<'a> + Send`,
// `dyn Tagged<'a> + Send + Sync` and `dyn Tagged<'a> + Sync`...
//
// Should be easy enough to do it with a macro...
Define a generic accessor on the Error trait
Use this Request type to handle passing generic types out of the trait object
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.
Unresolved questions
- What should the names of these functions be?
context/context_ref/provide_context/provide_context/request_contextmember/member_refprovide/request
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 zigs, 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;