Skip to content

Commit a73212f

Browse files
committed
Error handling improvements
Change how we handle error types to be more ergonomic: - Replace Cow with String: Diagnostics are serialized into JSON as soon as the function returns, which means that their value is copied right away. The performance improvement of using Cow is minimal in this case, but it has some ergonomic implications because we have to handle their lifetimes. By removing the explicit lifetimes, people can return Diagnostic values with static lifetimes which was not possible before. - Add `IntoDiagnostic` trait. This is a helper trait to facilitate transforming value types into Diagnostic. It gives external crates a better mechanism to transform values into `Diagnostic`. - Add features to implement `IntoDiagnostic` for anyhow, eyre, and miette error types. This helps people that use those creates to transform their errors into `Diagnostic` without double boxing their errors.
1 parent 4ee10b0 commit a73212f

File tree

21 files changed

+365
-126
lines changed

21 files changed

+365
-126
lines changed

README.md

+56-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ pip3 install cargo-lambda
4040

4141
See other installation options in [the Cargo Lambda documentation](https://www.cargo-lambda.info/guide/installation.html).
4242

43-
### Your first function
43+
## Your first function
4444

4545
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:
4646

@@ -71,6 +71,61 @@ async fn func(event: LambdaEvent<Value>) -> Result<Value, Error> {
7171
}
7272
```
7373

74+
## Understanding Lambda errors
75+
76+
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:
77+
78+
```json
79+
{
80+
"error_type": "the type of error raised",
81+
"error_message": "a string description of the error"
82+
}
83+
```
84+
85+
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.
86+
87+
### Implement your own Diagnostic
88+
89+
To get more descriptive `error_type` fields, you can implement `Into<Diagnostic>` for your error type. That gives you full control on what the `error_type` is:
90+
91+
```rust
92+
use lambda_runtime::{Diagnostic, Error, LambdaEvent};
93+
94+
#[derive(Debug)]
95+
struct ErrorResponse(&'static str);
96+
97+
impl Into<Diagnostic> for ErrorResponse {
98+
fn into(self) -> Diagnostic {
99+
Diagnostic {
100+
error_type: "MyErrorType".into(),
101+
error_message: self.0.to_string(),
102+
}
103+
}
104+
}
105+
106+
async fn handler(_event: LambdaEvent<()>) -> Result<(), ErrorResponse> {
107+
Err(ErrorResponse("this is an error response"))
108+
}
109+
```
110+
111+
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)
112+
113+
### Anyhow, Eyre, and Miette
114+
115+
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.
116+
117+
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` when the `lambda_runtime::IntoDiagnostic` trait is in scope. This trait exposes an `into_diagnostic` method that transforms those error types into a `Diagnostic`. This is an example that transforms an `anyhow::Error` into a `Diagnostic`:
118+
119+
```rust
120+
use lambda_runtime::{Diagnostic, IntoDiagnostic, LambdaEvent};
121+
122+
async fn handler(_event: LambdaEvent<Request>) -> Result<(), Diagnostic> {
123+
Err(anyhow::anyhow!("this is an error").into_diagnostic())
124+
}
125+
```
126+
127+
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).
128+
74129
## Building and deploying your Lambda functions
75130

76131
If you already have Cargo Lambda installed in your machine, run the next command to build your function:

examples/basic-error-anyhow/Cargo.toml

-10
This file was deleted.

examples/basic-error-anyhow/src/main.rs

-21
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[package]
2+
name = "basic-error-error-crates-integration"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
anyhow = "1"
8+
eyre = "0.6.12"
9+
lambda_runtime = { path = "../../lambda-runtime", features = ["anyhow", "eyre", "miette"] }
10+
miette = "7.2.0"
11+
serde = "1"
12+
tokio = { version = "1", features = ["macros"] }

examples/basic-error-anyhow/README.md renamed to examples/basic-error-error-crates-integration/README.md

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
# AWS Lambda Function Error Handling With `anyhow` Crate Example
1+
# AWS Lambda Function Error Handling with several popular error crates.
22

3-
This example shows how to use external error types like `anyhow::Error`.
3+
This example shows how to use external error types like `anyhow::Error`, `eyre::Report`, and `miette::Report`.
4+
5+
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.
46

57
## Build & Deploy
68

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
use lambda_runtime::{run, service_fn, Diagnostic, IntoDiagnostic, Error, LambdaEvent};
2+
use serde::Deserialize;
3+
4+
#[derive(Deserialize)]
5+
#[serde(rename_all = "camelCase")]
6+
enum ErrorType {
7+
Anyhow,
8+
Eyre,
9+
Miette,
10+
}
11+
12+
#[derive(Deserialize)]
13+
struct Request {
14+
error_type: ErrorType,
15+
}
16+
17+
fn anyhow_error() -> anyhow::Result<()> {
18+
anyhow::bail!("This is an error message from Anyhow");
19+
}
20+
21+
fn eyre_error() -> eyre::Result<()> {
22+
eyre::bail!("This is an error message from Eyre");
23+
}
24+
25+
fn miette_error() -> miette::Result<()> {
26+
miette::bail!("This is an error message from Miette");
27+
}
28+
29+
/// Transform an anyhow::Error, eyre::Report, or miette::Report into a lambda_runtime::Diagnostic.
30+
/// It does it by enabling the feature `anyhow`, `eyre` or `miette` in the runtime dependency,
31+
/// and importing the `IntoDiagnostic` trait, which enables
32+
/// the implementation of `into_diagnostic` for `anyhow::Error`, `eyre::Report`, and `miette::Report`.
33+
async fn function_handler(event: LambdaEvent<Request>) -> Result<(), Diagnostic> {
34+
match event.payload.error_type {
35+
ErrorType::Anyhow => anyhow_error().map_err(|e| e.into_diagnostic()),
36+
ErrorType::Eyre => eyre_error().map_err(|e| e.into_diagnostic()),
37+
ErrorType::Miette => miette_error().map_err(|e| e.into_diagnostic()),
38+
}
39+
}
40+
41+
#[tokio::main]
42+
async fn main() -> Result<(), Error> {
43+
run(service_fn(function_handler)).await
44+
}

examples/basic-error-diagnostic/Cargo.toml renamed to examples/basic-error-thiserror/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[package]
2-
name = "basic-error-diagnostic"
2+
name = "basic-error-thiserror"
33
version = "0.1.0"
44
edition = "2021"
55

examples/basic-error-diagnostic/src/main.rs renamed to examples/basic-error-thiserror/src/main.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ pub enum ExecutionError {
1313
Unexpected(String),
1414
}
1515

16-
impl<'a> From<ExecutionError> for Diagnostic<'a> {
17-
fn from(value: ExecutionError) -> Diagnostic<'a> {
16+
impl From<ExecutionError> for Diagnostic {
17+
fn from(value: ExecutionError) -> Diagnostic {
1818
let (error_type, error_message) = match value {
1919
ExecutionError::DatabaseError(err) => ("Retryable", err.to_string()),
2020
ExecutionError::Unexpected(err) => ("NonRetryable", err.to_string()),

lambda-http/Cargo.toml

+3
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ apigw_websockets = []
2323
alb = []
2424
pass_through = []
2525
tracing = ["lambda_runtime/tracing"]
26+
anyhow = ["lambda_runtime/anyhow"]
27+
eyre = ["lambda_runtime/eyre"]
28+
miette = ["lambda_runtime/miette"]
2629

2730
[dependencies]
2831
base64 = { workspace = true }

lambda-http/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ where
194194
S: Service<Request, Response = R, Error = E>,
195195
S::Future: Send + 'a,
196196
R: IntoResponse,
197-
E: std::fmt::Debug + for<'b> Into<Diagnostic<'b>>,
197+
E: std::fmt::Debug + Into<Diagnostic>,
198198
{
199199
lambda_runtime::run(Adapter::from(handler)).await
200200
}

lambda-http/src/streaming.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ pub async fn run_with_streaming_response<'a, S, B, E>(handler: S) -> Result<(),
2020
where
2121
S: Service<Request, Response = Response<B>, Error = E>,
2222
S::Future: Send + 'a,
23-
E: Debug + for<'b> Into<Diagnostic<'b>>,
23+
E: Debug + Into<Diagnostic>,
2424
B: Body + Unpin + Send + 'static,
2525
B::Data: Into<Bytes> + Send,
2626
B::Error: Into<Error> + Send + Debug,

lambda-runtime/Cargo.toml

+6
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,16 @@ readme = "../README.md"
1717
default = ["tracing"]
1818
tracing = ["lambda_runtime_api_client/tracing"]
1919
opentelemetry = ["opentelemetry-semantic-conventions"]
20+
anyhow = ["dep:anyhow"]
21+
eyre = ["dep:eyre"]
22+
miette = ["dep:miette"]
2023

2124
[dependencies]
25+
anyhow = { version = "1.0.86", optional = true }
2226
async-stream = "0.3"
2327
base64 = { workspace = true }
2428
bytes = { workspace = true }
29+
eyre = { version = "0.6.12", optional = true }
2530
futures = { workspace = true }
2631
http = { workspace = true }
2732
http-body = { workspace = true }
@@ -35,6 +40,7 @@ hyper-util = { workspace = true, features = [
3540
"tokio",
3641
] }
3742
lambda_runtime_api_client = { version = "0.11.1", path = "../lambda-runtime-api-client", default-features = false }
43+
miette = { version = "7.2.0", optional = true }
3844
opentelemetry-semantic-conventions = { version = "0.14", optional = true }
3945
pin-project = "1"
4046
serde = { version = "1", features = ["derive", "rc"] }

0 commit comments

Comments
 (0)