Introduction

AWS Lambda is undoubtedly becoming more and more popular. There is no shortage of materials related to the creation of an example function or even potential use cases. An often overlooked aspect, however, is error handling in AWS Lambda. Therefore, from the article you will learn:

  • How to invoke the AWS Lambda function in several ways?
  • How are handled and unhandled errors from AWS Lambda returned?
  • How can you handle both types of errors while calling the function?

We will need the Lambda function to answer these questions - in a very simple form to start with. Each call to this function will return the same model with a string in the response. In this article, I will not focus on the very process of creating and implementing functions. In all examples, I will use C# and .NET Core 2.1, although there is nothing to prevent you from using any language and runtime environment supported by AWS Lambda.

You can find all the examples ready for self-testing in my GitHub repository.

The function code looks like this:

public class SuccessLambda
{
    public dynamic Invoke() => new { Response = "AWS Lambda function invoked correctly!" };
}

Ways of calling the AWS Lambda function

We can call the created function in several ways. I will show you three of them and then analyze the obtained results in different cases.

AWS Management Console

The primary way to call the Lambda function is to use a dedicated view in the AWS Management Console. This method is often used in various tutorials on creating Lambda functions, but in real environments, it is used relatively rarely. In order to call a function in this way, go to the Lambda service in the AWS Management Console and search for the function you want to call. After entering the page of a specific function, find the Test button, which allows you to call the function directly:

Invocation from AWS console

After clicking the button, we have to properly configure the event by entering its name and body. The name is only a label for a specific configuration, while the body does not matter in this case - our function does not accept any parameters. However, it is important that it is a valid JSON file:

Configure test event

AWS CLI

Another way to call the function is to use the dedicated AWS CLI tool, which allows you to use the AWS API from the shell. This method is used much more often than using the AWS Management Console. It can be widely used in various types of scripts as well as various other AWS services. In order to call the created function, you need to use the following command:

aws lambda invoke `
  --function-name aws-lambda-error-handling-SuccessLambda-<your-unique-key> `
  response.json

In the command above, you need to complete the function name with a unique key generated during deployment. In addition to the name of the function, you have to also provide the file path where the response from the function will be written.

AWS SDK

The last analyzed method of using the Lambda function is invoking it from our code using the AWS SDK. Such programming tools are available for many programming languages. Due to the fact that in the example shown I am using C# and .NET Core, I will also present calling functions based on these technologies. However, there is nothing to prevent functions from being written in one language and called from another.

To be able to invoke a function from code in C#, we need to install the appropriate NuGet package. The mentioned library contains the AmazonLambdaClient class, which can be used, among other things, to invoke functions. The code invoking the created function might look like this:

public class AwsLambdaRunner
{
    private readonly IAmazonLambda _lambdaClient = new AmazonLambdaClient();

    public async Task<InvokeResponse> InvokeSuccessLambdaAsync()
    {
        const string successLambdaName = "aws-lambda-error-handling-SuccessLambda-<your-unique-key>";
        var invokeRequest = new InvokeRequest { FunctionName = successLambdaName };

        return await _lambdaClient.InvokeAsync(invokeRequest).ConfigureAwait(false);
    }
}

In the command above, the name of the function should also be extended with a unique key generated during the deployment. When invoked with AWS SDK, all information about the response to our request is obtained in the InvokeResponse object. It is returned by the InvokeAsync method.

Successful invocation result

In the default configuration, having the appropriate permissions, we will always receive a successful response to the request.

AWS Management Console

In the case of the AWS Management Console, we get the response directly in the same view where the function was called:

Console success invocation

As a result of the call, we receive an amount of information: from the response returned, through various metrics about a specific call, to a full log of events during the call.

AWS CLI

When calling with AWS CLI, we get the information about the request result. The response from the tool tells you whether the call has been handled correctly and what version of the function was called:

{
    "StatusCode": 200,
    "ExecutedVersion": "$LATEST"
}

Besides that, anything that is returned from the Lambda function will be saved to the path given when invoking. In this case it was response.json, in the same directory:

{"Response":"AWS Lambda function invoked correctly!"}

AWS SDK

The SDK call returns an object of the InvokeResponse class as a result. It contains various information - including StatusCode and ExecutedVersion, which we could also see in the AWS CLI call:

AWS SDK success invocation result

One of the properties of the returned object - Payload, contains the response from the Lambda function. In order to read it, we can create an appropriate class on the invocation side and deserialize the response to it:

[JsonObject]
public class SuccessLambdaResponseModel
{
    public string Response { get; }

    public SuccessLambdaResponseModel(string response)
    {
        Response = response;
    }
}

AWS SDK success invocation deserialized message

Erroneous invocation result

The previous examples were quite simplified. In real-world applications, the function code does not always execute correctly. This is due to various factors: errors in the code, unavailability of other services that the function uses, and errors in the function call itself.

Error while calling the function

The first type of errors are errors caused not so much by a function malfunction, but by problems with calling it. These include, for example, insufficient permissions or too many parallel calls (usually controlled by the Reserved concurrency parameter). For the purposes of analyzing the handling of such errors, I will simulate the problem related to too many simultaneous function calls.

AWS Management Console

A function call with this type of error looks like this in the AWS Management Console:

AWS Console fail invocation throttling

As you can see, the color of the window clearly indicates that the function call was not successful. In addition, the message informs you of the cause of this situation.

AWS CLI

When the same function is called by AWS CLI, we get this answer:

An error occurred (TooManyRequestsException) when calling the Invoke operation (reached max retries: 4): Rate Exceeded.

AWS SDK

If you try to call such a function from the AWS SDK, an exception will occur, raised by the object of the AmazonLambdaClient class:

Amazon.Lambda.Model.TooManyRequestsException: Rate Exceeded.

Worth to mention, that both AWS CLI and AWS SDK will retry the call for you when you receive this kind of error.

Error in the function runtime

Functions will more often fail due to some problem in the runtime. This time I will also use a properly prepared function for this purpose:

public class FailLambda
{
    public void Invoke() => throw new Exception("Exception thrown during AWS Lambda function runtime.");
}

In this case, the function always ends with an exception at runtime. Again, it’s worth analyzing the way the result is presented - depending on how the function was called.

AWS Management Console

Calling from the AWS console will produce the following result:

AWS Console fail invocation from function

The user interface clearly communicates that the call has failed. The window with the result is red. We receive detailed information in the field where we would normally get the answer from the function. We can see information about the error like its type, message, and the call stack.

AWS CLI

In this scenario, we get the following response by calling the function using AWS CLI:

{
    "StatusCode": 200,
    "FunctionError": "Unhandled",
    "ExecutedVersion": "$LATEST"
}

It is worth to notice that the AWS Lambda service returned a response with HTTP code 200, which is just like in the case of the correct call. This is one of the things that may be overlooked. It is easy to make the wrong assumption here that when an error occurs in our function, we will receive an HTTP code in response, indicating a client or server error. The result of this request is not about whether the function performed correctly or not. It concerns, however, whether calling the AWS Lambda service was successful. Information about whether the function itself was successful can be found in two places of the received response: the FunctionError field in the response, and in the response.json file. When calling Lambda with an error, in the FunctionError field we will see the value Unhandled. The response.json file contains detailed information about the error: its type, message, and call stack:

{
  "errorType": "Exception",
  "errorMessage": "Exception thrown during AWS Lambda function runtime.",
  "stackTrace": [
    "at AwsLambdaErrorHandling.Functions.FailLambda.Invoke() in C:\\<code-path>\\AwsLambdaErrorHandling.Functions\\FailLambda.cs:line 7"
  ]
}

AWS SDK

If we call the function from the code, we will see a result very similar to that from the AWS CLI call. The InvokeResponse class object that will be returned by the service looks like this:

AWS SDK response object after function fail invocation

In this case, the FunctionError field has the value Unhandled, and additional information can be found in the Payload field - in the form of a stream, which after reading takes the form known from the previous example:

{
  "errorType": "Exception",
  "errorMessage": "Exception thrown during AWS Lambda function runtime.",
  "stackTrace": [
    "at AwsLambdaErrorHandling.Functions.FailLambda.Invoke() in C:\\<code-path>\\AwsLambdaErrorHandling.Functions\\FailLambda.cs:line 7"
  ]
}

Summary of possible scenarios

The AWS Management Console, AWS CLI, and AWS SDK wrap the same call to AWS Lambda service. After analyzing various cases, we can distinguish the following scenarios:

AWS lambda invocation scenarios

  1. Successful function call. The function performed correctly. In the response, we get the response code 200 and the result of the function in the response body.
  2. Error while calling the function. There was a problem calling the AWS Lambda service. We receive a code from the class of client or server error codes in response. This may be the result of, for example, exceeding the call limit or insufficient permissions.
  3. Error in the function runtime. An error occurred while executing the function code. In response, we get the response code 200, the header X-Amz-Function-Error with the value Unhandled, as well as the error details in the response body.

Handling invocation errors

It is worth considering the enumerated scenarios when calling a function and construct the calling code in such a way as to cover all cases. It’s a good idea to consider creating a reusable client, especially when our solution uses Lambda function calls in multiple places. This approach will reduce code repetitions and introduce standardization within our project. When there are a lot of projects inside your organization that call functions, you might create a dedicated NuGet package.

It’s good to start building such a client with a simple call that just hides implementation details from the caller. This requires the use of the IAmazonLambda interface:

public class LambdaClient : ILambdaClient
{
    private readonly IAmazonLambda _amazonLambda;
    private readonly IJsonSerializer _jsonSerializer;

    public LambdaClient(IAmazonLambda amazonLambda, IJsonSerializer jsonSerializer)
    {
        _amazonLambda = amazonLambda;
        _jsonSerializer = jsonSerializer;
    }

    public async Task<TResponse> InvokeAsync<TRequest, TResponse>(string functionName, TRequest request)
    {
        var invokeRequest = new InvokeRequest
        {
            FunctionName = functionName,
            Payload = _jsonSerializer.Serialize(request)
        };

        var lambdaResponse = await _amazonLambda.InvokeAsync(invokeRequest).ConfigureAwait(false);
        return await _jsonSerializer.DeserializeAsync<TResponse>(lambdaResponse.Payload).ConfigureAwait(false);
    }
}

The client has two dependencies. One is related to the physical function call (the dependency comes from the Amazon.Lambda package), the other is related to the serialization and deserialization of models used in calls. LambdaClient implements the ILambdaClient interface, which is defined as follows:

public interface ILambdaClient
{
    Task<TResponse> InvokeAsync<TRequest, TResponse>(string functionName, TRequest request);
}

It is worth creating such an interface in case there is a need to mock the use of the created client.

Error handling while calling the function

The first type of error (related to a function call) is handled by an AWS-provided client within the SDK, i.e. AmazonLambdaClient. Upon receiving a response code other than 200, the client throws one of the exceptions. Each of the exceptions is a subclass of the base type AmazonLambdaException. From an error handling point of view, this could be a sufficient solution. However, you should consider creating your own exception type to don’t depend on the exceptions implemented in the external library:

public class LambdaInvocationException : Exception
{
    private const string ExceptionMessage = "Lambda call failed during invocation.";

    public LambdaInvocationException(Exception innerException) : base(ExceptionMessage, innerException) {}
}

Our client’s code can now be enhanced with handling of this type of error, introducing a simple improvement:

private async Task<InvokeResponse> InvokeLambda(InvokeRequest invokeRequest)
{
    try
    {
        return await _amazonLambda.InvokeAsync(invokeRequest).ConfigureAwait(false);
    }
    catch (AmazonLambdaException e)
    {
        throw new LambdaInvocationException(e);
    }
}

Error handling while function has a runtime error

Knowing what response to expect in the case of an error while the function is running, we can easily verify whether the response informs about the correct execution or about the error. In most cases it makes sense to create another dedicated exception expected in this scenario:

public class LambdaRuntimeException : Exception
{
    private const string ExceptionMessage = "Lambda call failed in runtime.";

    public LambdaRuntimeException() : base(ExceptionMessage) {}
}

On the client’s side, the verification of responses can come down to checking the appropriate header:

if (!string.IsNullOrWhiteSpace(lambdaResponse.FunctionError))
{
    throw new LambdaRuntimeException();
}

Remember that we also receive detailed information about the error. It is worth preparing an appropriate model and when such a case occurs, read it and pass it to the exception on our side:

public class LambdaRuntimeErrorModel
{
    public string ErrorType { get; set; }
    public string ErrorMessage { get; set; }
    public string[] StackTrace { get; set; }
}
if (!string.IsNullOrWhiteSpace(lambdaResponse.FunctionError))
{
    var runtimeError = await _jsonSerializer.DeserializeAsync<LambdaRuntimeErrorModel>(lambdaResponse.Payload).ConfigureAwait(false);
    throw new LambdaRuntimeException(runtimeError);
}

As a result of the applied improvements, the code of the created client looks as follows:

public class LambdaClient : ILambdaClient
{
    private readonly IAmazonLambda _amazonLambda;
    private readonly IJsonSerializer _jsonSerializer;

    public LambdaClient(IAmazonLambda amazonLambda, IJsonSerializer jsonSerializer)
    {
        _amazonLambda = amazonLambda;
        _jsonSerializer = jsonSerializer;
    }

    public async Task<TResponse> InvokeAsync<TRequest, TResponse>(string functionName, TRequest request)
    {
        var invokeRequest = new InvokeRequest
        {
            FunctionName = functionName,
            Payload = _jsonSerializer.Serialize(request)
        };

        var lambdaResponse = await InvokeLambda(invokeRequest).ConfigureAwait(false);

        if (!string.IsNullOrWhiteSpace(lambdaResponse.FunctionError))
        {
            var runtimeError = await _jsonSerializer.DeserializeAsync<LambdaRuntimeErrorModel>(lambdaResponse.Payload).ConfigureAwait(false);
            throw new LambdaRuntimeException(runtimeError);
        }

        return await _jsonSerializer.DeserializeAsync<TResponse>(lambdaResponse.Payload).ConfigureAwait(false);
    }

    private async Task<InvokeResponse> InvokeLambda(InvokeRequest invokeRequest)
    {
        try
        {
            return await _amazonLambda.InvokeAsync(invokeRequest).ConfigureAwait(false);
        }
        catch (AmazonLambdaException e)
        {
            throw new LambdaInvocationException(e);
        }
    }
}

Efficient Cloud That Suits Your Pocket

We have many years of experience with migrating, designing and optimizing cloud systems. Let us prepare a solution that suits your needs.

Schedule a call with our expert