RFC: Forward Compatible Errors
Status: RFC
Applies to: client
For a summarized list of proposed changes, see the Changes Checklist section.
This RFC defines an approach for making it forwards-compatible to convert unmodeled Unhandled
errors into modeled ones. This occurs as servers update their models to include errors that were previously unmodeled.
Currently, SDK errors are not forward compatible in this way. If a customer matches Unhandled
in addition to the _
branch and a new variant is added, they will fail to match the new variant. We currently handle this issue with enums by prevent useful information from being readable from the Unknown
variant.
This is related to ongoing work on the non_exhaustive_omitted_patterns
lint which would produce a compiler warning when a new variant was added even when _
was used.
Terminology
For purposes of discussion, consider the following error:
#[non_exhaustive]
pub enum AbortMultipartUploadError {
NoSuchUpload(NoSuchUpload),
Unhandled(Unhandled),
}
- Modeled Error: An error with an named variant, e.g.
NoSuchUpload
above - Unmodeled Error: Any other error, e.g. if the server returned
ValidationException
for the above operation. - Error code: All errors across all protocols provide a
code
, a unique method to identify an error across the service closure.
The user experience if this RFC is implemented
In the current version of the SDK, users match the Unhandled
variant. They can then read the code from the Unhandled
variant because Unhandled
implements the ProvideErrorMetadata
trait as well as the standard-library std::error::Error
trait.
Note: It's possible to write correct code today because the operation-level and service-level errors already expose
code()
viaProvideErrorMetadata
. This RFC describes mechanisms to guide customers to write forward-compatible code.
fn docs() {
match client.get_object().send().await {
Ok(obj) => { ... },
Err(e) => match e.into_service_error() {
GetObjectError::NotFound => { ... },
GetObjectError::Unhandled(err) if err.code() == "ValidationException" => { ... }
other => { /** do something with this variant */ }
}
}
}
We must instead guide customers into the following pattern:
fn docs() {
match client.get_object().send().await {
Ok(obj) => { ... },
Err(e) => match e.into_service_error() {
GetObjectError::NotFound => { ... },
err if err.code() == "ValidationException" => { ... },
err => warn!("{}", err.code()),
}
}
}
In this example, because customers are not matching on the Unhandled
variant explicitly this code is forward compatible for ValidationException
being introduced in the future.
Guiding Customers to this Pattern There are two areas we need to handle:
- Prevent customers from extracting useful information from
Unhandled
- Alert customers currently using unhandled what to use instead. For example, the following code is still problematic:
match err { GetObjectError::NotFound => { ... }, err @ GetObject::Unhandled(_) if err.code() == Some("ValidationException") => { ... } }
For 1
, we need to remove the ProvideErrorMetadata
trait implementation from Unhandled
. We would expose this isntead through a layer of indirection to enable code generated to code to still read the data.
For 2
, we would deprecate the Unhandled
variants with a message clearly indicating how this code should be written.
How to actually implement this RFC
Locking down Unhandled
In order to prevent accidental matching on Unhandled
, we need to make it hard to extract useful information from Unhandled
itself. We will do this by removing the ProvideErrorMetadata
trait implementation and exposing the following method:
#[doc(hidden)]
/// Introspect the error metadata of this error.
///
/// This method should NOT be used from external code because matching on `Unhandled` directly is a backwards-compatibility
/// hazard. See `RFC-0039` for more information.
pub fn introspect(&self) -> impl ProvideErrorMetadata + '_ {
struct Introspected<'a>(&'a Unhandled);
impl ProvideErrorMetadata for Introspected { ... }
Introspected(self)
}
Generated code would this use introspect
when supporting top-level ErrorMetadata
(e.g. for aws_sdk_s3::Error
).
Deprecating the Variant
The Unhandled
variant will be deprecated to prevent users from matching on it inadvertently.
enum GetObjectError {
NotFound(NotFound),
#[deprecated("Matching on `Unhandled` directly is a backwards compatibility hazard. Use `err if err.error_code() == ...` instead. See [here](<docs about using errors>) for more information.")]
Unhandled(Unhandled)
}
Changes checklist
-
Generate code to deprecate unhandled variants. Determine the best way to allow
Unhandled
to continue to be constructed in client code -
Generate code to deprecate the
Unhandled
variant for the service meta-error. Consider how this interacts with non-service errors. -
Update
Unhandled
to make it useless on its own and expose information via anIntrospect
doc hidden struct. - Update developer guide to address this issue.
- Changelog & Upgrade Guidance