aws-lambda-rust-runtime: [RFC] Error handling in Rust runtime

Currently, the Lambda Rust runtime declares a HandlerError type that developers can use to return an error from their handler method. While this approach works, it forces developers to write more verbose code to handle all errors and generate the relevant HandlerError objects. Developers would like to return errors automatically using the ? operator (#23).

For example:

fn my_handler(e: CustomEvent, c: lambda::Context) -> Result<CustomOutput, Error> {
    let i = event.age.parse::<u8>?;
    ...
}

In an error response, the Lambda runtime API expects two parameters: errorType and errorMessage. Optionally, we can also attach a stack trace to the error.

{
    "errorMessage" : "Error parsing age data.",
    "errorType" : "InvalidAgeException"
}

To generate an error message field we need the Err variant from the handler Result to support the Display trait. This allows us to support any Display type in the result - Error types inherently supports Display too. However, we cannot identify a way to automatically generate the error type field given a Display-compatible object that uses stable features. To address this, we plan to introduce a new trait in the runtime:

pub trait ErrorExt {
    fn error_type(&self) -> &str;
}

We’d like to deprecate this trait in the future and rely on the type name intrinsic (which is currently blocked on specialization). For context, see #1428.

The name ErrorExt comes from the extension trait conventions RFC. Based on feedback, we are open to changing this.

The runtime crate itself will provide the implementation of the ErrorExt trait for the most common errors in the standard library. Developers will have to implement the trait themselves in their own errors. We may consider adding a procedural macro to the runtime crate to automatically generate the trait implementation.

In summary, the proposed changes are:

  1. The handler type will accept any object that implements Display and ErrorExt in its Err variant.
  2. The runtime crate will use the Display trait to extract an error message and use the ISO-8859-1 charset.
  3. The runtime crate will call the error_type() function to get the value for the errorType field.
  4. If the RUST_BACKTRACE environment variable is set to 1, the runtime will use the backtrace crate to collect a stack trace as soon as the error is received.

The new handler type definition will be:

pub trait Handler<Event, Output, Error> 
where 
    Event: From<Vec<u8>>,
    Output: Into<Vec<u8>>,
    Error: Display + ErrorExt + Send + Sync,
{
    fn run(&mut self, event: Event, ctx: Context) -> Result<Output, Error>;
}

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 5
  • Comments: 15 (15 by maintainers)

Most upvoted comments

Thank you all for the comments so far. We experimented with the various methodologies on our side. Based on the results, we concluded that the best option is to rely on the failure crate. The proposed changes are:

  • The handler type in the runtime crate will accept an additional generic for errors. The generic will expect the following traits
impl<F, E, O, ER> Runtime<F, E, O, ER>
where
    F: Handler<E, O, ER>,
    E: serde::de::DeserializeOwned,
    O: serde::Serialize,
    ER: AsFail + LambdaErrorExt + Send + Sync + Debug,

In the process we will rename the generics to make the code more readable

  • The LambdaErrorExt trait defines the error_type() function to extract the parameter for the Lambda runtime API
pub trait LambdaErrorExt {
    fn error_type(&self) -> &str;
}
  • We will move the LambdaErrorExt trait definition to a separate lambda-error crate - related to the comment on #53.
  • The lambda-error crate will also provide implementations of the LambdaErrorExt for all of the Error types in the standard library

With the changes outlined above, developers will have to depend on the failure crate for their functions. They can either use the concrete failure::Error type for simple functions or implement their own custom errors.

basic.rs

use failure::{bail, Error as FailError};

...

fn my_handler(e: CustomEvent, c: lambda::Context) -> Result<CustomOutput, FailError> {
    if e.first_name == "" {
        error!("Empty first name in request {}", c.aws_request_id);
        bail!("Empty first name");
    }

    Ok(CustomOutput {
        message: format!("Hello, {}!", e.first_name),
    })
}

custom.rs

use failure::Fail;

...

#[derive(Debug, Fail)]
#[fail(display = "custom error")]
enum CustomError {
    #[fail(display = "{}", _0)]
    ParseIntError(#[fail(cause)] std::num::ParseIntError),

    #[fail(display = "Generic, unknown error")]
    Generic,
}

impl LambdaErrorExt for CustomError {
    fn error_type(&self) -> &str {
        "MyCustomError"
    }
}

...

fn my_handler(e: CustomEvent, c: lambda::Context) -> Result<CustomOutput, CustomError> {
    if e.first_name == "" {
        error!("Empty first name in request {}", c.aws_request_id);
        return Err(CustomError::Generic);
    }

    let _age_num: u8 = e.age.parse().map_err(CustomError::ParseIntError)?;

    Ok(CustomOutput {
        message: format!("Hello, {}!", e.first_name),
    })
}

Changes for this and #53 are staged in #63 - let me know what you guys think. I’d like to merge and prepare for a 0.2 release by the end of next week.

Just as a data point, an advantage of using failure::Error is that stack traces could originate from other crates that use failure, not just from code specifically written for Lambda.