Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Error handling improvements #907

Merged
merged 5 commits into from
Jul 12, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
calavera marked this conversation as resolved.
Show resolved Hide resolved
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.

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
}
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
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"]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since there is no standard way of documenting features yet (rust-lang/rfcs#3485), shall we add # enables From<T> for Diagnostic for anyhow error types, see ReadMe for more info here?
Add the same for eyre, and miette or merge into a single comment.

The features are described in the ReadMe, but that's likely to be missed on the first read.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good idea. I've documented them all since I was there.

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
Loading