Closed TethysSvensson closed 4 months ago
Feel free to open a PR to improve the support of those errors 🙏
I don't see any way to fix this without compromising on #880.
You will need to choose whether you want to automatically support error types that implement Display
or not. In either case, if your choice does not match the users, then they will need to implement a work-around in their code.
I will list a few different options for how to proceed from here. Let me know which one you prefer, and I will implement a PR for that.
This is the context as I see and and how I will evaluate the different options:
Display
.Diagnostic
.With the current code, all error types besides the few types blessed in diagnostic.rs
will be slightly annoying to deal with.
Your choices are to either convert to one of the few blessed types:
lambda_runtime_api_client::BoxError
aka Box<dyn std::error::Error + Sync + Send>
Box<dyn std::error::Error>
Box<T> where T: std::error::Error
String
and &'static str
.Or alternatively you can make you can define your own custom error type like this:
struct MyErrorType(...);
impl<'a> From<MyErrorType> for Diagnostic<'a> {
fn from(value: MyErrorType) -> Diagnostic<'a> {
...
}
}
Note, you cannot implement From/Into
for types external to your own crate, so you will need to do one of these conversion if you use e.g. anyhow
.
Reverting would fix the majority use-case, but it would re-introduce friction in the minority use-case. It would become difficult to use a custom conversion for your own error types that already implement Display.
The only real way to achieve custom conversion for your own types would be something like this:
#[derive(Debug, thiserror::Error)]
struct MyError { ... }
struct AnnoyingErrorWrapper(MyError);
impl<'a> From<AnnoyingErrorWrapper> for Diagnostic<'a> {
fn from(value: AnnoyingErrorWrapper) -> Diagnostic<'a> {
...
}
}
impl From<MyError> for AnnoyingErrorWrapper {
fn from(value: MyError) -> AnnoyingErrorWrapper {
AnnoyingErrorWrapper(value)
}
}
As I see it, there is no way to introduce some friction to either the majority or minority use-cases, however that friction could be made much smaller than in either option 1 or 2.
I think the best way would be to re-add the blanket impl, but also make it easier to return custom diagnostics. I see two non-conflicting ways we could reduce the friction.
It is currently impossible to directly return a Diagnostic<'static>
from your handler, because we require for<'b> Into<Diagnostic<'b>>
. This is because Diagnostic<'static>
only has an implementation of Into<Diagnostic<'static>>
despite being coercible into any Diagnostic<'a>
.
We could fix this by using a custom trait IntoDiagnostic<'a>
instead of using Into<Diagnostic<'a>>
. That way the user would be able to create a helper function like this:
async fn handler(event: lambda_runtime::LambdaEvent<()>) -> Result<(), Diagnostic<'static>> {
real_handler(event)
.await
.map_err(|e: MyError| -> Diagnostic {
...
})
}
async fn real_handler(_event: lambda_runtime::LambdaEvent<()>) -> Result<(), MyError> {
...
}
We could also add an additional type CustomDiagnostic<'a>
, which did not have an automatic blanket impl, where users could implement their own conversion logic. It could look something like this:
impl<'a> From<MyError> for CustomDiagnostic<'a> {
fn from(value: MyError) -> Self {
todo!()
}
}
async fn handler(event: lambda_runtime::LambdaEvent<()>) -> Result<(), CustomDiagnostic<'static>> {
func1(&event).await?;
func2(&event).await?;
Ok(())
}
async fn func1(event: &lambda_runtime::LambdaEvent<()>) -> Result<(), MyError> {
todo!()
}
async fn func2(event: &lambda_runtime::LambdaEvent<()>) -> Result<(), MyError> {
todo!()
}
We could create a macro for creating custom conversion types. It could look something like this:
create_error_wrapper! { MyError => AnnoyingErrorWrapper with converter }
fn converter(value: MyError) -> Diagnostic<'static> {
...
}
This would then unfold to this:
struct AnnoyingErrorWrapper(MyError);
impl<'a> From<AnnoyingErrorWrapper> for Diagnostic<'a> {
fn from(value: AnnoyingErrorWrapper) -> Diagnostic<'a> {
converter(value.0)
}
}
impl From<MyError> for AnnoyingErrorWrapper {
fn from(value: MyError) -> AnnoyingErrorWrapper {
AnnoyingErrorWrapper(value)
}
}
It would also be possible to do some of the ergonomic improvements without re-adding the blanket impl. Specifically the first and third ergonomic improvements would still make sense in this case.
Personally I think the best option is Option 3, with friction reduction number 1. I would not mind also adding number 2 and 3 though.
Let's start with option 1. I don't fully understand what the benefit of option 2 is(I might need more information), and I'd rather avoid macros unless they're absolutely necessary.
Additionally, I think we can add other common error implementations, like std::io::Error
that you brought up. I've been thinking more about implementations for errors crates like anyhow, and it might be ok to add implementations for them behind a feature flag.
I am confused about whether you mean options or the friction reductions I've discussed.
Basically my outline had are two orthogonal choices:
My suggestion was to bring back the blanket impl to benefit the majority case, but then add some ergonomic improvements to make the minority case less cumbersome.
I consider giving people the ability to completely customize the error to return more important that returning string messages for any type of error. We're not going to bring back the blanket implementation. Returning an anyhow message is not that much complicated with the current version, this seems to work:
async fn function_handler(event: LambdaEvent<Request>) -> Result<(), Error> {
Err(anyhow!("ooops").into())
}
We've never guaranteed backwards compatibility for this exact reason, we need to ensure that we can provide access to all features that Lambda provides, including returning the right error payloads, and we need to be able to make these kind of breaking changes before we stabilize the runtime.
I'm happy to merge further improvements on top of the current implementation though.
Sure, but if you do that, then you:
lambda_runtime::Error
.error_type
, since the error type will always be "alloc::boxed::Box<dyn core::error::Error + core::marker::Send + core::marker::Sync>"
.As a naive user who until the upgrade to 0.12 was happily returning some impl of Error
from his lambda handler, I can only second @TethysSvensson's point here. This is a surprising breaking change for everyone who didn't even know what Diagnostic
was until the crate upgrade broke something. Sure it's easy to fix. But making the default path more complicated to simplify some customization is usually a bad tradeoff.
- You loose the benefit of the diagnostic, which is the
error_type
, since the error type will always be"alloc::boxed::Box<dyn core::error::Error + core::marker::Send + core::marker::Sync>"
.
I would argue that both anyhow::Error
and "alloc::boxed::Box<dyn core::error::Error + core::marker::Send + core::marker::Sync>"
are bad error types.
But making the default path more complicated to simplify some customization is usually a bad tradeoff.
@tim3z error types are not a customization, they are part of Lambda's feature set. Unfortunately, we've been ignoring it for way too long, and now we're paying the price. This is definitely on me for not approaching the problem sooner. This has also created some bad practices, like parsing the error message to observe and understand why a function call failed, where the error_type exists for this specific reason.
As I mention earlier, I'm happy to review improvements upon this breaking change to improve the experience, as long as defining diagnostics doesn't get buried again.
[EDIT] The best possible way to find a solution is through PRs. There are several error handling examples in the examples
directory. Feel free to propose a solution that makes those examples work without making it complicated to implement diagnostics.
I've made some improvements in https://github.com/awslabs/aws-lambda-rust-runtime/pull/907. Let me know what you think.
This issue is now closed. Comments on closed issues are hard for our team to see. If you need more assistance, please either tag a team member or open a new issue that references this one.
With the release of v0.12.0, it is no longer possible to use error types defined externally to your own crate. This is caused by the inclusion of #897.
Example:
Note that it is not possible to implement
Into<Diagnostic<'a>>
yourself for any of these types because of the orphan rules.