Each error variant gets its own struct, which can hold error-specific contextual information.
Except for the `Unhandled` variant, both the error enum and the details on each variant are extensible.
The `Unhandled` variant should move the error source into a struct so that its type can be hidden.
Otherwise, the code generated errors are already aligned with the goals of this RFC.
Approaches from other projects
------------------------------
### `std::io::Error`
The standard library uses an `Error` struct with an accompanying `ErrorKind` enum
for its IO error. Roughly:
```rust
#[derive(Debug)]
#[non_exhaustive]
pubenumErrorKind{
NotFound,
// ... omitted ...
Other,
}
#[derive(Debug)]
pubstructError{
kind:ErrorKind,
source:Box<dynstd::error::Error+Send+Sync>,
}
```
What this error does well:
- It is extensible since the `ErrorKind` is non-exhaustive
- It has an `Other` error type that can be instantiated by users in unit tests,
making it easier to unit test error handling
What could be improved:
- There isn't an ergonomic way to add programmatically accessible error-specific context
to this error in the future
- The source error can be downcasted, which could be a trap for backwards compatibility.
### Hyper 1.0
Hyper is has outlined [some problems they want to address with errors](https://github.com/hyperium/hyper/blob/bd7928f3dd6a8461f0f0fdf7ee0fd95c2f156f88/docs/ROADMAP.md#errors)
for the coming 1.0 release. To summarize:
- It's difficult to match on specific errors (Hyper 0.x's `Error` relies
on `is_x` methods for error matching rather than enum matching).
- Error reporters duplicate information since the hyper 0.x errors include the display of their error sources
-`Error::source()` can leak internal dependencies
Opaque Error Sources
--------------------
There is [discussion in the errors working group](https://github.com/rust-lang/project-error-handling/issues/53)
about how to avoid leaking internal dependency error types through error source downcasting. One option is to
create an opaque error wrapping new-type that removes the ability to downcast to the other library's error.
This, however, can be circumvented via unsafe code, and also breaks the ability for error reporters to
properly display the error (for example, if the error has backtrace information, that would be
inaccessible to the reporter).
This situation might improve if the nightly `request_value`/`request_ref`/`provide` functions on
`std::error::Error` are stabilized, since then contextual information needed for including things
such as a backtrace could still be retrieved through the opaque error new-type.
This RFC proposes that error types from other libraries not be directly exposed in the API, but rather,
be exposed indirectly through `Error::source` as `&dyn Error + 'static`.
Errors should not require downcasting to be useful. Downcasting the error's source should be
a last resort, and with the understanding that the type could change at a later date with no
compile-time guarantees.
Error Proposal
--------------
Taking a customer's perspective, there are two broad categories of errors:
1.**Actionable:** Errors that can/should influence program flow; where it's useful to
do different work based on additional error context or error variant information
2.**Informative:** Errors that inform that something went wrong, but where
it's not useful to match on the error to change program flow
This RFC proposes that a consistent pattern be introduced to cover these two use cases for
all errors in the public API for the Rust runtime crates and generated client crates.
### Actionable error pattern
Actionable errors are represented as enums. If an error variant has an error source or additional contextual
information, it must use a separate context struct that is referenced via tuple in the enum. For example:
```rust
pubenumError{
// Good: This is exhaustive and uses a tuple, but its sole member is an extensible struct with private fields
VariantA(VariantA),
// Bad: The fields are directly exposed and can't have accessor methods. The error
// source type also can't be changed at a later date since.
#[non_exhaustive]
VariantB{
some_additional_info:u32,
source:AnotherError// AnotherError is from this crate
},
// Bad: There's no way to add additional contextual information to this error in the future, even
// though it is non-exhaustive. Changing it to a tuple or struct later leads to compile errors in existing
// match statements.
#[non_exhaustive]
VariantC,
// Bad: Not extensible if additional context is added later (unless that context can be added to `AnotherError`)
#[non_exhaustive]
VariantD(AnotherError),
// Bad: Not extensible. If new context is added later (for example, a second endpoint), there's no way to name it.
#[non_exhaustive]
VariantE(Endpoint,AnotherError),
// Bad: Exposes another library's error type in the public API,
// which makes upgrading or replacing that library a breaking change
#[non_exhaustive]
VariantF{
source:http::uri::InvalidUri
},
// Bad: The error source type is public, and even though its a boxed error, it won't
// be possible to change it to an opaque error type later (for example, if/when
// opaque errors become practical due to standard library stabilizations).
#[non_exhaustive]
VariantG{
source:Box<dynError+Send+Sync+'static>,
}
}
pubstructVariantA{
some_field:u32,
// This is private, so it's fine to reference the external library's error type
source:http::uri::InvalidUri
}
implVariantA{
fnsome_field(&self)->u32{
self.some_field
}
}
```
Error variants that contain a source must return it from the `Error::source` method.
The `source` implementation _should not_ use the catch all (`_`) match arm, as this makes it easy to miss
adding a new error variant's source at a later date.
The error `Display` implementation _must not_ include the source in its output: