Hi all,
I'm writing a library that wraps AWS services and it's fair to say that their SDK errors are ... complex. All services derive from this type defined in the smithy api, and there's a few others (e.g. from "watchers")
#[derive(Debug)]
pub enum SdkError<E, R> {
ConstructionFailure(ConstructionFailure),
TimeoutError(TimeoutError),
DispatchFailure(DispatchFailure),
ResponseError(ResponseError<R>),
ServiceError(ServiceError<E, R>),
}
...
impl<E, R> Error for SdkError<E, R>
where
E: Error + 'static,
R: Debug,
{
...
}
in this case, from SdkError in aws_smithy_runtime_api::client::result - Rust
The main problem is the use of generics, because to be able to include these in my wrapper type I need to make it generic too, and I'd rather avoid that.
My sledgehammer approach is to wrap those with my own error type, and the only thing they all have in common is a big ol Box
of tricks... (I'm using some macros from derive_more
here in case you're wondering)
#[derive(Debug, Display, Error, From)]
pub enum Fail {
MyError(#[error(not(source))] String),
DependencyError {
source: Box<dyn std::error::Error + 'static + Send>,
},
}
but I'd like to be a little bit nicer, and also protect myself from including Fail
inside Fail
(which I've done a few times already, it's easily done) and have an entry in my enum for each one of the services that can fail and preserve their structure. But I'm not getting very far. I thought it would be possible to add an entry and put the most basic constraints on them like here
SmithyApiError(
aws_smithy_runtime_api::client::result::SdkError<
Box<dyn std::error::Error + Send + 'static>,
Box<dyn std::fmt::Debug + Send + 'static>,
>,
),
so I'd get the Send + 'static
that my code needs, and the impl Error for SdkError
would get what it needs too. But this fails to compile, which I can fix if I disable the derive_more
Error derivation and implement it by hand if necessary... although I still need to write lots of constructor wrappers because the types don't actually line up since I have Box
here and the actual errors have types with known sizes. I have no way to convert between a SdkError<E, R>
and an SdkError<Box<...>, Box<...>>
anyways so I'm not sure if this is even a feasible approach.
A small step I can make is to mark the Fail
as #[non_exhaustive]
and then only ever create instances via From
s tailored to each of the SDK errors, which at least stops me re-wrapping my own errors or introducing something unexpected.
impl<E, R> From<aws_smithy_runtime_api::client::result::SdkError<E, R>> for Fail
where
E: Error + Send + 'static,
R: Debug + Send + 'static,
{
fn from(e: aws_smithy_runtime_api::client::result::SdkError<E, R>) -> Fail {
Fail::DependencyError {
source: Box::new(e),
}
}
}
impl<O, E> From<aws_smithy_runtime_api::client::waiters::error::WaiterError<O, E>> for Fail
where
O: Debug + Send + 'static,
E: Error + Debug + Send + 'static,
{
fn from(e: aws_smithy_runtime_api::client::waiters::error::WaiterError<O, E>) -> Fail {
Fail::DependencyError {
source: Box::new(e),
}
}
}
which does help with error creation, but it still feels like I'm still not giving enough information back to my users. They don't know anything about the sources and can't really inspect them.
Also, maybe I'm asking the wrong question and the question I should be asking is: how should I be idiomatically wrapping up errors that come from third party libraries when I have no choice but to bubble them up to the user? (and also, in this case the user would probably appreciate getting specific AWS service errors because these sorts of things are usually related to security policies and credentials and environments and as much context as possible is always welcome).
(Apologies for this long and rambling post, I'm definitely not really sure what to ask for here )