Skip to content

Commit

Permalink
Error handling improvements
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
calavera committed Jul 5, 2024
1 parent 4ee10b0 commit 5f7f9c7
Show file tree
Hide file tree
Showing 21 changed files with 364 additions and 124 deletions.
57 changes: 56 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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 `Into<Diagnostic>` 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 Into<Diagnostic> for ErrorResponse {
fn into(self) -> Diagnostic {
Diagnostic {
error_type: "MyErrorType".into(),
error_message: self.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` 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`:

```rust
use lambda_runtime::{Diagnostic, IntoDiagnostic, LambdaEvent};

async fn handler(_event: LambdaEvent<Request>) -> Result<(), Diagnostic> {
Err(anyhow::anyhow!("this is an error").into_diagnostic())
}
```

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:
Expand Down
10 changes: 0 additions & 10 deletions examples/basic-error-anyhow/Cargo.toml

This file was deleted.

21 changes: 0 additions & 21 deletions examples/basic-error-anyhow/src/main.rs

This file was deleted.

File renamed without changes.
12 changes: 12 additions & 0 deletions examples/basic-error-error-crates-integration/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"] }
Original file line number Diff line number Diff line change
@@ -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

Expand Down
44 changes: 44 additions & 0 deletions examples/basic-error-error-crates-integration/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
use lambda_runtime::{run, service_fn, Diagnostic, IntoDiagnostic, 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,
/// and importing the `IntoDiagnostic` trait, which enables
/// the implementation of `into_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_diagnostic()),
ErrorType::Eyre => eyre_error().map_err(|e| e.into_diagnostic()),
ErrorType::Miette => miette_error().map_err(|e| e.into_diagnostic()),
}
}

#[tokio::main]
async fn main() -> Result<(), Error> {
run(service_fn(function_handler)).await
}
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[package]
name = "basic-error-diagnostic"
name = "basic-error-thiserror"
version = "0.1.0"
edition = "2021"

Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
Expand Down
3 changes: 3 additions & 0 deletions lambda-http/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ apigw_websockets = []
alb = []
pass_through = []
tracing = ["lambda_runtime/tracing"]
anyhow = ["lambda_runtime/anyhow"]
eyre = ["lambda_runtime/eyre"]
miette = ["lambda_runtime/miette"]

[dependencies]
base64 = { workspace = true }
Expand Down
2 changes: 1 addition & 1 deletion lambda-http/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion lambda-http/src/streaming.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 6 additions & 0 deletions lambda-runtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,16 @@ readme = "../README.md"
default = ["tracing"]
tracing = ["lambda_runtime_api_client/tracing"]
opentelemetry = ["opentelemetry-semantic-conventions"]
anyhow = ["dep:anyhow"]
eyre = ["dep:eyre"]
miette = ["dep:miette"]

[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 }
Expand All @@ -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"] }
Expand Down
Loading

0 comments on commit 5f7f9c7

Please sign in to comment.