Repository: awslabs/aws-lambda-rust-runtime (language: rust)

Issue:
Regression in v0.12.0: External error types no longer work with lambda_runtime::run
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:

```rust
// Works in both v0.11.3 and v0.12.0
type Error = Box<dyn std::error::Error>;

// Works in v0.11.3 but not in v0.12.0
// type Error = std::io::Error;

// Works in v0.11.3 but not in v0.12.0
// type Error = anyhow::Error;

// Works in neither v0.11.3 nor v0.12.0
// type Error = lambda_runtime::Diagnostic<'static>;

async fn handler(_event: lambda_runtime::LambdaEvent<()>) -> Result<(), Error> {
    Ok(())
}

#[tokio::main]
async fn main() {
    lambda_runtime::run(lambda_runtime::service_fn(handler))
        .await
        .unwrap();
}
```

Note that it is not possible to implement `Into<Diagnostic<'a>>` yourself for any of these types because of the orphan rules.


Discussion / hints:
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:

* Almost all users will want to use error types that implement `Display`.
* The majority of users would prefer an implementation that just works out of the box. They do not care about customizing their conversion.
* A minority of users would like to customize their conversion to `Diagnostic`.
* We want to support both the majority user-case and the niche use-case with as little friction as possible.
* If friction has to be introduced, make it as small as possible, but prioritize minimizing friction in smaller in the majority case.

### Option 1: Keeping things as they are

With the current code, all error types besides the few types [blessed in `diagnostic.rs`](https://github.com/awslabs/aws-lambda-rust-runtime/blob/f8cc32de3166f277375dbf1203aa3936509c1cf0/lambda-runtime/src/diagnostic.rs#L56) will be slightly annoying to deal with.

Your choices are to either convert to one of the few blessed types:

* A type internal to lambda_runtime
* A few different box types, specifically one of:
  * `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:

```rust
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`.

### Option 2: Partially revert #897 to re-add the blanked impl

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:

```rust
#[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)
    }
}
```

### Option 3: Partial revert with ergonomic improvements

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.

#### Friction reduction 1:

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:

```rust
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> {
    ...
}
```

#### Friction reduction 2:

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:

```rust
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!()
}
```

#### Friction reduction 3:

We could create a macro for creating custom conversion types. It could look something like this:

```rust
create_error_wrapper! { MyError => AnnoyingErrorWrapper with converter }

fn converter(value: MyError) -> Diagnostic<'static> {
    ...
}
```

This would then unfold to this:

```rust
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)
    }
}
```

### Option 4: Ergonomic improvements without a blanket impl

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.

### Conclusion

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:

* Whether to bring back the blanket impl
* Which ergonomic improvements to use.

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:

```rust
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:

* Introduce friction into the majority case instead of the minority case. It is easy to support both, the question is where you prefer a small amount of friction to be introduced.
* You require double-boxing all errors, since anyhow is already boxed and so is `lambda_runtime::Error`.
* 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>"`.
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.

Fix patch:
diff --git a/README.md b/README.md
index 44ec1983..31ba399b 100644
--- a/README.md
+++ b/README.md
@@ -40,7 +40,7 @@ pip3 install cargo-lambda
 
 See other installation options in [the Cargo Lambda documentation](https://www.cargo-lambda.info/guide/installation.html).
 
-### Your first function
+## Your first function
 
 To create your first function, run Cargo Lambda with the [subcommand `new`](https://www.cargo-lambda.info/commands/new.html). This command will generate a Rust package with the initial source code for your function:
 
@@ -71,6 +71,61 @@ async fn func(event: LambdaEvent<Value>) -> Result<Value, Error> {
 }
 ```
 
+## Understanding Lambda errors
+
+when a function invocation fails, AWS Lambda expects you to return an object that can be serialized into JSON structure with the error information. This structure is represented in the following example:
+
+```json
+{
+  "error_type": "the type of error raised",
+  "error_message": "a string description of the error"
+}
+```
+
+The Rust Runtime for Lambda uses a struct called `Diagnostic` to represent function errors internally. The runtime implements the converstion of several general errors types, like `std::error::Error`, into `Diagnostic`. For these general implementations, the `error_type` is the name of the value type returned by your function. For example, if your function returns `lambda_runtime::Error`, the `error_type` will be something like `alloc::boxed::Box<dyn core::error::Error + core::marker::Send + core::marker::Sync>`, which is not very descriptive.
+
+### Implement your own Diagnostic
+
+To get more descriptive `error_type` fields, you can implement `From` for your error type. That gives you full control on what the `error_type` is:
+
+```rust
+use lambda_runtime::{Diagnostic, Error, LambdaEvent};
+
+#[derive(Debug)]
+struct ErrorResponse(&'static str);
+
+impl From<ErrorResponse> for Diagnostic {
+    fn from(error: ErrorResponse) -> Diagnostic {
+        Diagnostic {
+            error_type: "MyErrorType".into(),
+            error_message: error.0.to_string(),
+        }
+    }
+}
+
+async fn handler(_event: LambdaEvent<()>) -> Result<(), ErrorResponse> {
+  Err(ErrorResponse("this is an error response"))
+}
+```
+
+We recommend you to use the [thiserror crate](https://crates.io/crates/thiserror) to declare your errors. You can see an example on how to integrate `thiserror` with the Runtime's diagnostics in our [example repository](https://github.com/awslabs/aws-lambda-rust-runtime/tree/main/examples/basic-error-thiserror)
+
+### Anyhow, Eyre, and Miette
+
+Popular error crates like Anyhow, Eyre, and Miette provide their own error types that encapsulate other errors. There is no direct transformation of those errors into `Diagnostic`, but we provide feature flags for each one of those crates to help you integrate them with your Lambda functions.
+
+If you enable the features `anyhow`, `eyre`, or `miette` in the `lambda_runtime` dependency of your package. The error types provided by those crates can have blanket transformations into `Diagnostic`. These features expose an `From<T> for Diagnostic` implementation that transforms those error types into a `Diagnostic`. This is an example that transforms an `anyhow::Error` into a `Diagnostic`:
+
+```rust
+use lambda_runtime::{Diagnostic, LambdaEvent};
+
+async fn handler(_event: LambdaEvent<Request>) -> Result<(), Diagnostic> {
+  Err(anyhow::anyhow!("this is an error").into())
+}
+```
+
+You can see more examples on how to use these error crates in our [example repository](https://github.com/awslabs/aws-lambda-rust-runtime/tree/main/examples/basic-error-error-crates-integration). 
+
 ## Building and deploying your Lambda functions
 
 If you already have Cargo Lambda installed in your machine, run the next command to build your function:
diff --git a/examples/basic-error-anyhow/Cargo.toml b/examples/basic-error-anyhow/Cargo.toml
deleted file mode 100644
index a0ff62db..00000000
--- a/examples/basic-error-anyhow/Cargo.toml
+++ /dev/null
@@ -1,10 +0,0 @@
-[package]
-name = "basic-error-anyhow"
-version = "0.1.0"
-edition = "2021"
-
-[dependencies]
-anyhow = "1"
-lambda_runtime = { path = "../../lambda-runtime" }
-serde = "1"
-tokio = { version = "1", features = ["macros"] }
diff --git a/examples/basic-error-anyhow/src/main.rs b/examples/basic-error-anyhow/src/main.rs
deleted file mode 100644
index cbca84fd..00000000
--- a/examples/basic-error-anyhow/src/main.rs
+++ /dev/null
@@ -1,21 +0,0 @@
-use anyhow::bail;
-use lambda_runtime::{service_fn, Error, LambdaEvent};
-use serde::Deserialize;
-
-#[derive(Deserialize)]
-struct Request {}
-
-/// Return anyhow::Result in the main body for the Lambda function.
-async fn function_handler(_event: LambdaEvent<Request>) -> anyhow::Result<()> {
-    bail!("This is an error message");
-}
-
-#[tokio::main]
-async fn main() -> Result<(), Error> {
-    lambda_runtime::run(service_fn(|event: LambdaEvent<Request>| async move {
-        function_handler(event)
-            .await
-            .map_err(Into::<Box<dyn std::error::Error>>::into)
-    }))
-    .await
-}
diff --git a/examples/basic-error-anyhow/.gitignore b/examples/basic-error-error-crates-integration/.gitignore
similarity index 100%
rename from examples/basic-error-anyhow/.gitignore
rename to examples/basic-error-error-crates-integration/.gitignore
diff --git a/examples/basic-error-error-crates-integration/Cargo.toml b/examples/basic-error-error-crates-integration/Cargo.toml
new file mode 100644
index 00000000..741ec713
--- /dev/null
+++ b/examples/basic-error-error-crates-integration/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "basic-error-error-crates-integration"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+anyhow = "1"
+eyre = "0.6.12"
+lambda_runtime = { path = "../../lambda-runtime", features = ["anyhow", "eyre", "miette"] }
+miette = "7.2.0"
+serde = "1"
+tokio = { version = "1", features = ["macros"] }
diff --git a/examples/basic-error-anyhow/README.md b/examples/basic-error-error-crates-integration/README.md
similarity index 54%
rename from examples/basic-error-anyhow/README.md
rename to examples/basic-error-error-crates-integration/README.md
index b659c283..2e46c55d 100644
--- a/examples/basic-error-anyhow/README.md
+++ b/examples/basic-error-error-crates-integration/README.md
@@ -1,6 +1,8 @@
-# AWS Lambda Function Error Handling With `anyhow` Crate Example
+# AWS Lambda Function Error Handling with several popular error crates.
 
-This example shows how to use external error types like `anyhow::Error`.
+This example shows how to use external error types like `anyhow::Error`, `eyre::Report`, and `miette::Report`.
+
+To use the integrations with these crates, you need to enable to respective feature flag in the runtime which provides the implemetation of `into_diagnostic` for specific error types provided by these crates.
 
 ## Build & Deploy
 
diff --git a/examples/basic-error-error-crates-integration/src/main.rs b/examples/basic-error-error-crates-integration/src/main.rs
new file mode 100644
index 00000000..f4048584
--- /dev/null
+++ b/examples/basic-error-error-crates-integration/src/main.rs
@@ -0,0 +1,44 @@
+use lambda_runtime::{run, service_fn, Diagnostic, Error, LambdaEvent};
+use serde::Deserialize;
+
+#[derive(Deserialize)]
+#[serde(rename_all = "camelCase")]
+enum ErrorType {
+    Anyhow,
+    Eyre,
+    Miette,
+}
+
+#[derive(Deserialize)]
+struct Request {
+    error_type: ErrorType,
+}
+
+fn anyhow_error() -> anyhow::Result<()> {
+    anyhow::bail!("This is an error message from Anyhow");
+}
+
+fn eyre_error() -> eyre::Result<()> {
+    eyre::bail!("This is an error message from Eyre");
+}
+
+fn miette_error() -> miette::Result<()> {
+    miette::bail!("This is an error message from Miette");
+}
+
+/// Transform an anyhow::Error, eyre::Report, or miette::Report into a lambda_runtime::Diagnostic.
+/// It does it by enabling the feature `anyhow`, `eyre` or `miette` in the runtime dependency.
+/// Those features enable the implementation of `From<T> for Diagnostic` 
+/// for `anyhow::Error`, `eyre::Report`, and `miette::Report`.
+async fn function_handler(event: LambdaEvent<Request>) -> Result<(), Diagnostic> {
+    match event.payload.error_type {
+        ErrorType::Anyhow => anyhow_error().map_err(|e| e.into()),
+        ErrorType::Eyre => eyre_error().map_err(|e| e.into()),
+        ErrorType::Miette => miette_error().map_err(|e| e.into()),
+    }
+}
+
+#[tokio::main]
+async fn main() -> Result<(), Error> {
+    run(service_fn(function_handler)).await
+}
diff --git a/examples/basic-error-diagnostic/.gitignore b/examples/basic-error-thiserror/.gitignore
similarity index 100%
rename from examples/basic-error-diagnostic/.gitignore
rename to examples/basic-error-thiserror/.gitignore
diff --git a/examples/basic-error-diagnostic/Cargo.toml b/examples/basic-error-thiserror/Cargo.toml
similarity index 94%
rename from examples/basic-error-diagnostic/Cargo.toml
rename to examples/basic-error-thiserror/Cargo.toml
index b81ef730..d7c7d725 100644
--- a/examples/basic-error-diagnostic/Cargo.toml
+++ b/examples/basic-error-thiserror/Cargo.toml
@@ -1,5 +1,5 @@
 [package]
-name = "basic-error-diagnostic"
+name = "basic-error-thiserror"
 version = "0.1.0"
 edition = "2021"
 
diff --git a/examples/basic-error-diagnostic/README.md b/examples/basic-error-thiserror/README.md
similarity index 100%
rename from examples/basic-error-diagnostic/README.md
rename to examples/basic-error-thiserror/README.md
diff --git a/examples/basic-error-diagnostic/src/main.rs b/examples/basic-error-thiserror/src/main.rs
similarity index 90%
rename from examples/basic-error-diagnostic/src/main.rs
rename to examples/basic-error-thiserror/src/main.rs
index 11f68d4b..403309bf 100644
--- a/examples/basic-error-diagnostic/src/main.rs
+++ b/examples/basic-error-thiserror/src/main.rs
@@ -13,8 +13,8 @@ pub enum ExecutionError {
     Unexpected(String),
 }
 
-impl<'a> From<ExecutionError> for Diagnostic<'a> {
-    fn from(value: ExecutionError) -> Diagnostic<'a> {
+impl From<ExecutionError> for Diagnostic {
+    fn from(value: ExecutionError) -> Diagnostic {
         let (error_type, error_message) = match value {
             ExecutionError::DatabaseError(err) => ("Retryable", err.to_string()),
             ExecutionError::Unexpected(err) => ("NonRetryable", err.to_string()),
diff --git a/lambda-http/Cargo.toml b/lambda-http/Cargo.toml
index 1165084b..fc822b95 100644
--- a/lambda-http/Cargo.toml
+++ b/lambda-http/Cargo.toml
@@ -22,7 +22,11 @@ apigw_http = []
 apigw_websockets = []
 alb = []
 pass_through = []
-tracing = ["lambda_runtime/tracing"]
+tracing = ["lambda_runtime/tracing"] # enables access to the Tracing utilities
+opentelemetry = ["lambda_runtime/opentelemetry"] # enables access to the OpenTelemetry layers and utilities
+anyhow = ["lambda_runtime/anyhow"] # enables From<T> for Diagnostic for anyhow error types, see README.md for more info
+eyre = ["lambda_runtime/eyre"] # enables From<T> for Diagnostic for eyre error types, see README.md for more info
+miette = ["lambda_runtime/miette"] # enables From<T> for Diagnostic for miette error types, see README.md for more info
 
 [dependencies]
 base64 = { workspace = true }
diff --git a/lambda-http/src/lib.rs b/lambda-http/src/lib.rs
index 90e59867..233d6992 100644
--- a/lambda-http/src/lib.rs
+++ b/lambda-http/src/lib.rs
@@ -194,7 +194,7 @@ where
     S: Service<Request, Response = R, Error = E>,
     S::Future: Send + 'a,
     R: IntoResponse,
-    E: std::fmt::Debug + for<'b> Into<Diagnostic<'b>>,
+    E: std::fmt::Debug + Into<Diagnostic>,
 {
     lambda_runtime::run(Adapter::from(handler)).await
 }
diff --git a/lambda-http/src/streaming.rs b/lambda-http/src/streaming.rs
index ad3471d3..a93408b4 100644
--- a/lambda-http/src/streaming.rs
+++ b/lambda-http/src/streaming.rs
@@ -20,7 +20,7 @@ pub async fn run_with_streaming_response<'a, S, B, E>(handler: S) -> Result<(),
 where
     S: Service<Request, Response = Response<B>, Error = E>,
     S::Future: Send + 'a,
-    E: Debug + for<'b> Into<Diagnostic<'b>>,
+    E: Debug + Into<Diagnostic>,
     B: Body + Unpin + Send + 'static,
     B::Data: Into<Bytes> + Send,
     B::Error: Into<Error> + Send + Debug,
diff --git a/lambda-runtime/Cargo.toml b/lambda-runtime/Cargo.toml
index 9e56d05b..371b32f7 100644
--- a/lambda-runtime/Cargo.toml
+++ b/lambda-runtime/Cargo.toml
@@ -15,13 +15,18 @@ readme = "../README.md"
 
 [features]
 default = ["tracing"]
-tracing = ["lambda_runtime_api_client/tracing"]
-opentelemetry = ["opentelemetry-semantic-conventions"]
+tracing = ["lambda_runtime_api_client/tracing"] # enables access to the Tracing utilities
+opentelemetry = ["opentelemetry-semantic-conventions"] # enables access to the OpenTelemetry layers and utilities
+anyhow = ["dep:anyhow"] # enables From<T> for Diagnostic for anyhow error types, see README.md for more info
+eyre = ["dep:eyre"] # enables From<T> for Diagnostic for eyre error types, see README.md for more info
+miette = ["dep:miette"] # enables From<T> for Diagnostic for miette error types, see README.md for more info
 
 [dependencies]
+anyhow = { version = "1.0.86", optional = true }
 async-stream = "0.3"
 base64 = { workspace = true }
 bytes = { workspace = true }
+eyre = { version = "0.6.12", optional = true }
 futures = { workspace = true }
 http = { workspace = true }
 http-body = { workspace = true }
@@ -35,6 +40,7 @@ hyper-util = { workspace = true, features = [
     "tokio",
 ] }
 lambda_runtime_api_client = { version = "0.11.1", path = "../lambda-runtime-api-client", default-features = false }
+miette = { version = "7.2.0", optional = true }
 opentelemetry-semantic-conventions = { version = "0.14", optional = true }
 pin-project = "1"
 serde = { version = "1", features = ["derive", "rc"] }
diff --git a/lambda-runtime/src/diagnostic.rs b/lambda-runtime/src/diagnostic.rs
index 9a7230a7..c03ce284 100644
--- a/lambda-runtime/src/diagnostic.rs
+++ b/lambda-runtime/src/diagnostic.rs
@@ -1,5 +1,5 @@
 use serde::{Deserialize, Serialize};
-use std::{any::type_name, borrow::Cow};
+use std::any::type_name;
 
 use crate::{deserializer::DeserializeError, Error};
 
@@ -14,114 +14,144 @@ use crate::{deserializer::DeserializeError, Error};
 /// [`error_type`][`Diagnostic::error_type`] is derived from the type name of
 /// the original error with [`std::any::type_name`] as a fallback, which may
 /// not be reliable for conditional error handling.
-/// You can define your own error container that implements `Into<Diagnostic>`
-/// if you need to handle errors based on error types.
+///
+/// To get more descriptive [`error_type`][`Diagnostic::error_type`] fields, you can implement `From` for your error type.
+/// That gives you full control on what the `error_type` is.
 ///
 /// Example:
 /// ```
 /// use lambda_runtime::{Diagnostic, Error, LambdaEvent};
-/// use std::borrow::Cow;
 ///
 /// #[derive(Debug)]
-/// struct ErrorResponse(Error);
+/// struct ErrorResponse(&'static str);
 ///
-/// impl<'a> Into<Diagnostic<'a>> for ErrorResponse {
-///     fn into(self) -> Diagnostic<'a> {
+/// impl From<ErrorResponse> for Diagnostic {
+///     fn from(error: ErrorResponse) -> Diagnostic {
 ///         Diagnostic {
 ///             error_type: "MyError".into(),
-///             error_message: self.0.to_string().into(),
+///             error_message: error.0.to_string(),
 ///         }
 ///     }
 /// }
 ///
 /// async fn function_handler(_event: LambdaEvent<()>) -> Result<(), ErrorResponse> {
-///    // ... do something
-///    Ok(())
+///    Err(ErrorResponse("this is an error response"))
 /// }
 /// ```
 #[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
 #[serde(rename_all = "camelCase")]
-pub struct Diagnostic<'a> {
-    /// Error type.
-    ///
-    /// `error_type` is derived from the type name of the original error with
-    /// [`std::any::type_name`] as a fallback.
-    /// Please implement your own `Into<Diagnostic>` if you need more reliable
-    /// error types.
-    pub error_type: Cow<'a, str>,
-    /// Error message.
+pub struct Diagnostic {
+    /// `error_type` is the type of exception or error returned by the function.
+    /// Use this field to categorize the different kinds of errors that your function
+    /// might experience.
     ///
-    /// `error_message` is the output from the [`Display`][std::fmt::Display]
-    /// implementation of the original error as a fallback.
-    pub error_message: Cow<'a, str>,
+    /// In standard implementations, `error_type` is derived from the type name of the original error with
+    /// [`std::any::type_name`], however this is not descriptive enough for an error type.
+    /// Implement your own `Into<Diagnostic>` to return a more descriptive error type.
+    pub error_type: String,
+    /// `error_message` is a string expression of the error.
+    /// In standard implementations, it's the output from the [`Display`][std::fmt::Display]
+    /// implementation of the original error.
+    pub error_message: String,
 }
 
-impl<'a> From<DeserializeError> for Diagnostic<'a> {
+impl From<DeserializeError> for Diagnostic {
     fn from(value: DeserializeError) -> Self {
         Diagnostic {
-            error_type: type_name::<DeserializeError>().into(),
-            error_message: value.to_string().into(),
+            error_type: type_name_of_val(&value),
+            error_message: value.to_string(),
         }
     }
 }
 
-impl<'a> From<Error> for Diagnostic<'a> {
+impl From<Error> for Diagnostic {
     fn from(value: Error) -> Self {
         Diagnostic {
-            error_type: type_name::<Error>().into(),
-            error_message: value.to_string().into(),
+            error_type: type_name_of_val(&value),
+            error_message: value.to_string(),
         }
     }
 }
 
-impl<'a, T> From<Box<T>> for Diagnostic<'a>
-where
-    T: std::error::Error,
-{
-    fn from(value: Box<T>) -> Self {
+impl From<Box<dyn std::error::Error>> for Diagnostic {
+    fn from(value: Box<dyn std::error::Error>) -> Self {
         Diagnostic {
-            error_type: type_name::<T>().into(),
-            error_message: value.to_string().into(),
+            error_type: type_name_of_val(&value),
+            error_message: value.to_string(),
         }
     }
 }
 
-impl<'a> From<Box<dyn std::error::Error>> for Diagnostic<'a> {
-    fn from(value: Box<dyn std::error::Error>) -> Self {
+impl From<std::convert::Infallible> for Diagnostic {
+    fn from(value: std::convert::Infallible) -> Self {
         Diagnostic {
-            error_type: type_name::<Box<dyn std::error::Error>>().into(),
-            error_message: value.to_string().into(),
+            error_type: type_name_of_val(&value),
+            error_message: value.to_string(),
         }
     }
 }
 
-impl<'a> From<std::convert::Infallible> for Diagnostic<'a> {
-    fn from(value: std::convert::Infallible) -> Self {
+impl From<String> for Diagnostic {
+    fn from(value: String) -> Self {
         Diagnostic {
-            error_type: type_name::<std::convert::Infallible>().into(),
-            error_message: value.to_string().into(),
+            error_type: type_name_of_val(&value),
+            error_message: value.to_string(),
         }
     }
 }
 
-impl<'a> From<String> for Diagnostic<'a> {
-    fn from(value: String) -> Self {
+impl From<&'static str> for Diagnostic {
+    fn from(value: &'static str) -> Self {
         Diagnostic {
-            error_type: type_name::<String>().into(),
-            error_message: value.into(),
+            error_type: type_name_of_val(&value),
+            error_message: value.to_string(),
         }
     }
 }
 
-impl<'a> From<&'static str> for Diagnostic<'a> {
-    fn from(value: &'static str) -> Self {
+impl From<std::io::Error> for Diagnostic {
+    fn from(value: std::io::Error) -> Self {
+        Diagnostic {
+            error_type: type_name_of_val(&value),
+            error_message: value.to_string(),
+        }
+    }
+}
+
+#[cfg(feature = "anyhow")]
+impl From<anyhow::Error> for Diagnostic {
+    fn from(value: anyhow::Error) -> Diagnostic {
+        Diagnostic {
+            error_type: type_name_of_val(&value),
+            error_message: value.to_string(),
+        }
+    }
+}
+
+#[cfg(feature = "eyre")]
+impl From<eyre::Report> for Diagnostic {
+    fn from(value: eyre::Report) -> Diagnostic {
         Diagnostic {
-            error_type: type_name::<&'static str>().into(),
-            error_message: value.into(),
+            error_type: type_name_of_val(&value),
+            error_message: value.to_string(),
         }
     }
 }
 
+#[cfg(feature = "miette")]
+impl From<miette::Report> for Diagnostic {
+    fn from(value: miette::Report) -> Diagnostic {
+        Diagnostic {
+            error_type: type_name_of_val(&value),
+            error_message: value.to_string(),
+        }
+    }
+}
+
+pub(crate) fn type_name_of_val<T>(_: T) -> String {
+    type_name::<T>().into()
+}
+
 #[cfg(test)]
 mod test {
     use super::*;
@@ -141,4 +171,34 @@ mod test {
         let actual: Value = serde_json::to_value(actual).expect("failed to serialize diagnostic");
         assert_eq!(expected, actual);
     }
+
+    #[cfg(feature = "anyhow")]
+    #[test]
+    fn test_anyhow_integration() {
+        use anyhow::Error as AnyhowError;
+        let error: AnyhowError = anyhow::anyhow!("anyhow error");
+        let diagnostic: Diagnostic = error.into();
+        assert_eq!(diagnostic.error_type, "&anyhow::Error");
+        assert_eq!(diagnostic.error_message, "anyhow error");
+    }
+
+    #[cfg(feature = "eyre")]
+    #[test]
+    fn test_eyre_integration() {
+        use eyre::Report;
+        let error: Report = eyre::eyre!("eyre error");
+        let diagnostic: Diagnostic = error.into();
+        assert_eq!(diagnostic.error_type, "&eyre::Report");
+        assert_eq!(diagnostic.error_message, "eyre error");
+    }
+
+    #[cfg(feature = "miette")]
+    #[test]
+    fn test_miette_integration() {
+        use miette::Report;
+        let error: Report = miette::miette!("miette error");
+        let diagnostic: Diagnostic = error.into();
+        assert_eq!(diagnostic.error_type, "&miette::eyreish::Report");
+        assert_eq!(diagnostic.error_message, "miette error");
+    }
 }
diff --git a/lambda-runtime/src/layers/api_response.rs b/lambda-runtime/src/layers/api_response.rs
index 7c963f68..e744cde1 100644
--- a/lambda-runtime/src/layers/api_response.rs
+++ b/lambda-runtime/src/layers/api_response.rs
@@ -51,8 +51,7 @@ impl<S, EventPayload, Response, BufferedResponse, StreamingResponse, StreamItem,
     }
 }
 
-impl<'a, S, EventPayload, Response, BufferedResponse, StreamingResponse, StreamItem, StreamError>
-    Service<LambdaInvocation>
+impl<S, EventPayload, Response, BufferedResponse, StreamingResponse, StreamItem, StreamError> Service<LambdaInvocation>
     for RuntimeApiResponseService<
         S,
         EventPayload,
@@ -63,7 +62,7 @@ impl<'a, S, EventPayload, Response, BufferedResponse, StreamingResponse, StreamI
         StreamError,
     >
 where
-    S: Service<LambdaEvent<EventPayload>, Response = Response, Error = Diagnostic<'a>>,
+    S: Service<LambdaEvent<EventPayload>, Response = Response, Error = Diagnostic>,
     EventPayload: for<'de> Deserialize<'de>,
     Response: IntoFunctionResponse<BufferedResponse, StreamingResponse>,
     BufferedResponse: Serialize,
@@ -74,7 +73,7 @@ where
     type Response = http::Request<Body>;
     type Error = BoxError;
     type Future =
-        RuntimeApiResponseFuture<'a, S::Future, Response, BufferedResponse, StreamingResponse, StreamItem, StreamError>;
+        RuntimeApiResponseFuture<S::Future, Response, BufferedResponse, StreamingResponse, StreamItem, StreamError>;
 
     fn poll_ready(&mut self, cx: &mut task::Context<'_>) -> task::Poll<Result<(), Self::Error>> {
         self.inner
@@ -120,21 +119,21 @@ where
     }
 }
 
-fn build_event_error_request<'a, T>(request_id: &'a str, err: T) -> Result<http::Request<Body>, BoxError>
+fn build_event_error_request<T>(request_id: &str, err: T) -> Result<http::Request<Body>, BoxError>
 where
-    T: Into<Diagnostic<'a>> + Debug,
+    T: Into<Diagnostic> + Debug,
 {
     error!(error = ?err, "Request payload deserialization into LambdaEvent<T> failed. The handler will not be called. Log at TRACE level to see the payload.");
     EventErrorRequest::new(request_id, err).into_req()
 }
 
 #[pin_project(project = RuntimeApiResponseFutureProj)]
-pub enum RuntimeApiResponseFuture<'a, F, Response, BufferedResponse, StreamingResponse, StreamItem, StreamError> {
+pub enum RuntimeApiResponseFuture<F, Response, BufferedResponse, StreamingResponse, StreamItem, StreamError> {
     Future(
         #[pin] F,
         String,
         PhantomData<(
-            &'a (),
+            (),
             Response,
             BufferedResponse,
             StreamingResponse,
@@ -145,10 +144,10 @@ pub enum RuntimeApiResponseFuture<'a, F, Response, BufferedResponse, StreamingRe
     Ready(Option<Result<http::Request<Body>, BoxError>>),
 }
 
-impl<'a, F, Response, BufferedResponse, StreamingResponse, StreamItem, StreamError> Future
-    for RuntimeApiResponseFuture<'a, F, Response, BufferedResponse, StreamingResponse, StreamItem, StreamError>
+impl<F, Response, BufferedResponse, StreamingResponse, StreamItem, StreamError> Future
+    for RuntimeApiResponseFuture<F, Response, BufferedResponse, StreamingResponse, StreamItem, StreamError>
 where
-    F: Future<Output = Result<Response, Diagnostic<'a>>>,
+    F: Future<Output = Result<Response, Diagnostic>>,
     Response: IntoFunctionResponse<BufferedResponse, StreamingResponse>,
     BufferedResponse: Serialize,
     StreamingResponse: Stream<Item = Result<StreamItem, StreamError>> + Unpin + Send + 'static,
diff --git a/lambda-runtime/src/layers/panic.rs b/lambda-runtime/src/layers/panic.rs
index 036b747b..c76348ac 100644
--- a/lambda-runtime/src/layers/panic.rs
+++ b/lambda-runtime/src/layers/panic.rs
@@ -1,9 +1,7 @@
-use crate::{Diagnostic, LambdaEvent};
+use crate::{diagnostic::type_name_of_val, Diagnostic, LambdaEvent};
 use futures::{future::CatchUnwind, FutureExt};
 use pin_project::pin_project;
-use std::{
-    any::Any, borrow::Cow, fmt::Debug, future::Future, marker::PhantomData, panic::AssertUnwindSafe, pin::Pin, task,
-};
+use std::{any::Any, fmt::Debug, future::Future, marker::PhantomData, panic::AssertUnwindSafe, pin::Pin, task};
 use tower::Service;
 use tracing::error;
 
@@ -33,9 +31,9 @@ impl<'a, S, Payload> Service<LambdaEvent<Payload>> for CatchPanicService<'a, S>
 where
     S: Service<LambdaEvent<Payload>>,
     S::Future: 'a,
-    S::Error: Into<Diagnostic<'a>> + Debug,
+    S::Error: Into<Diagnostic> + Debug,
 {
-    type Error = Diagnostic<'a>;
+    type Error = Diagnostic;
     type Response = S::Response;
     type Future = CatchPanicFuture<'a, S::Future>;
 
@@ -71,9 +69,9 @@ pub enum CatchPanicFuture<'a, F> {
 impl<'a, F, T, E> Future for CatchPanicFuture<'a, F>
 where
     F: Future<Output = Result<T, E>>,
-    E: Into<Diagnostic<'a>> + Debug,
+    E: Into<Diagnostic> + Debug,
 {
-    type Output = Result<T, Diagnostic<'a>>;
+    type Output = Result<T, Diagnostic>;
 
     fn poll(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> task::Poll<Self::Output> {
         use task::Poll;
@@ -98,20 +96,15 @@ where
 }
 
 impl<'a, F> CatchPanicFuture<'a, F> {
-    fn build_panic_diagnostic(err: &Box<dyn Any + Send>) -> Diagnostic<'a> {
-        let error_type = type_name_of_val(&err);
-        let msg = if let Some(msg) = err.downcast_ref::<&str>() {
+    fn build_panic_diagnostic(err: &Box<dyn Any + Send>) -> Diagnostic {
+        let error_message = if let Some(msg) = err.downcast_ref::<&str>() {
             format!("Lambda panicked: {msg}")
         } else {
             "Lambda panicked".to_string()
         };
         Diagnostic {
-            error_type: Cow::Borrowed(error_type),
-            error_message: Cow::Owned(msg),
+            error_type: type_name_of_val(err),
+            error_message,
         }
     }
 }
-
-fn type_name_of_val<T>(_: T) -> &'static str {
-    std::any::type_name::<T>()
-}
diff --git a/lambda-runtime/src/lib.rs b/lambda-runtime/src/lib.rs
index c3e68cc4..76d0562a 100644
--- a/lambda-runtime/src/lib.rs
+++ b/lambda-runtime/src/lib.rs
@@ -112,7 +112,7 @@ pub async fn run<A, F, R, B, S, D, E>(handler: F) -> Result<(), Error>
 where
     F: Service<LambdaEvent<A>, Response = R>,
     F::Future: Future<Output = Result<R, F::Error>>,
-    F::Error: for<'a> Into<Diagnostic<'a>> + fmt::Debug,
+    F::Error: Into<Diagnostic> + fmt::Debug,
     A: for<'de> Deserialize<'de>,
     R: IntoFunctionResponse<B, S>,
     B: Serialize,
diff --git a/lambda-runtime/src/requests.rs b/lambda-runtime/src/requests.rs
index 0e2eb59a..535f1e8e 100644
--- a/lambda-runtime/src/requests.rs
+++ b/lambda-runtime/src/requests.rs
@@ -158,11 +158,11 @@ fn test_event_completion_request() {
 // /runtime/invocation/{AwsRequestId}/error
 pub(crate) struct EventErrorRequest<'a> {
     pub(crate) request_id: &'a str,
-    pub(crate) diagnostic: Diagnostic<'a>,
+    pub(crate) diagnostic: Diagnostic,
 }
 
 impl<'a> EventErrorRequest<'a> {
-    pub(crate) fn new(request_id: &'a str, diagnostic: impl Into<Diagnostic<'a>>) -> EventErrorRequest<'a> {
+    pub(crate) fn new(request_id: &'a str, diagnostic: impl Into<Diagnostic>) -> EventErrorRequest<'a> {
         EventErrorRequest {
             request_id,
             diagnostic: diagnostic.into(),
@@ -224,8 +224,8 @@ mod tests {
         let req = EventErrorRequest {
             request_id: "id",
             diagnostic: Diagnostic {
-                error_type: std::borrow::Cow::Borrowed("InvalidEventDataError"),
-                error_message: std::borrow::Cow::Borrowed("Error parsing event data"),
+                error_type: "InvalidEventDataError".into(),
+                error_message: "Error parsing event data".into(),
             },
         };
         let req = req.into_req().unwrap();
diff --git a/lambda-runtime/src/runtime.rs b/lambda-runtime/src/runtime.rs
index 56c8efad..1c676480 100644
--- a/lambda-runtime/src/runtime.rs
+++ b/lambda-runtime/src/runtime.rs
@@ -72,7 +72,7 @@ impl<'a, F, EventPayload, Response, BufferedResponse, StreamingResponse, StreamI
 where
     F: Service<LambdaEvent<EventPayload>, Response = Response>,
     F::Future: Future<Output = Result<Response, F::Error>>,
-    F::Error: Into<Diagnostic<'a>> + Debug,
+    F::Error: Into<Diagnostic> + Debug,
     EventPayload: for<'de> Deserialize<'de>,
     Response: IntoFunctionResponse<BufferedResponse, StreamingResponse>,
     BufferedResponse: Serialize,
@@ -207,7 +207,7 @@ fn wrap_handler<'a, F, EventPayload, Response, BufferedResponse, StreamingRespon
 where
     F: Service<LambdaEvent<EventPayload>, Response = Response>,
     F::Future: Future<Output = Result<Response, F::Error>>,
-    F::Error: Into<Diagnostic<'a>> + Debug,
+    F::Error: Into<Diagnostic> + Debug,
     EventPayload: for<'de> Deserialize<'de>,
     Response: IntoFunctionResponse<BufferedResponse, StreamingResponse>,
     BufferedResponse: Serialize,
@@ -257,7 +257,7 @@ mod endpoint_tests {
     use httpmock::prelude::*;
 
     use lambda_runtime_api_client::Client;
-    use std::{borrow::Cow, env, sync::Arc};
+    use std::{env, sync::Arc};
     use tokio_stream::StreamExt;
 
     #[tokio::test]
@@ -324,8 +324,8 @@ mod endpoint_tests {
     #[tokio::test]
     async fn test_error_response() -> Result<(), Error> {
         let diagnostic = Diagnostic {
-            error_type: Cow::Borrowed("InvalidEventDataError"),
-            error_message: Cow::Borrowed("Error parsing event data"),
+            error_type: "InvalidEventDataError".into(),
+            error_message: "Error parsing event data".into(),
         };
         let body = serde_json::to_string(&diagnostic)?;
 


Test patch: