author-image

David Hunt

How to handle Lambda errors in API Gateway

When creating/maintaining APIs, errors will inevitably occur, whether that’s caused by the client request or caused by an error on the server end. Either way a good API will help the requesting client understand what has happened so that they can modify their request or simply report an issue.

At Fathom, we commonly use Amazon API Gateway with AWS Lambda to create serverless APIs. You can find out more about serverless in our other blog post.

Amazon API Gateway is an AWS fully managed service that makes it easy to create APIs at any scale. AWS Lambda allows us to run code in the cloud without having to provision/manage any servers.

API Gateway supports two types of APIs, RESTful APIs and Websocket APIs. Within RESTful APIs, AWS provides two variations, the HTTP API and the REST API. You can learn more about these in the AWS Docs

The HTTP API is the newer of the two APIs and Amazon recommend using it for most new applications, citing lower cost and improved performance as two of its benefits. Another benefit of HTTP APIs over REST APIs is that the make it easier to return error responses. In this blog post I will walk you through how you can return error messages to a client application with the HTTP API.

Setup

For this blog we will create a very basic API, making use of API Gateway to create an API endpoint and route requests to a Lambda function which will be used to investigate error responses.

API Review Screen

We are going to create our API via the AWS console as the main goal of the this post is about how to return error responses, and not the best practice for creating your APIs. At Fathom we prefer to use tools like Terraform which allow for infrastructure creation as code. You can learn more in our Terraform blog

Lambda Function

Lambda Function Creation Screen

  1. First open up the AWS console and go to the Lambda services.
  2. Click Create Function
  3. Select Author from scratch
  4. Provide a name for the function, for this example, I’m going to call the function error-test.
  5. For runtime choose Node.js. At time of writing Node.js 14 is LTS.
  6. Hit Create Function at the bottom.

API Gateway

  1. Navigate to API Gateway.
  2. Click Create API
  3. Select HTTP API
  4. Click Add Integration under the Integrations section
    1. Select Lambda from the dropdown
    2. Now select the function we created in the previous section. i.e error-test
  5. Choose an AWS region.
  6. Give the API a name, we’re just going to call it error-test.
  7. In configure routes screen
    1. Set Method to GET
    2. Set Resource path to /
    3. Ensure the integration target is the error-test Lambda function
  8. You can leave the rest of the settings as default and click Create

API Review Screen

Now we have a basic API created. Performing a GET request to the base route (/) of our API will invoke the Lambda function that we have created.

You should be brought to a page with all the details about the API. Find the Invoke URL and perform a GET request on the endpoint.

curl --location --request GET 'https://example.execute-api.eu-west-1.amazonaws.com'

You should receive a response like: “Hello from Lambda!”. This is what is returned from your Lambda function.

Returning errors

So what happens if we have an issue in our Lambda function? What if we’re accessing data from a database and an error occurs? How does our API deal with errors?

Lambda Code Screen

Well let’s do a test and find out. Open up your Lambda function in the console and replace the return line with a throw new Error('This is an error'). Perform a GET request on the endpoint like before.

You should get

{
    “message”: “Internal Server Error”
}

Well, that’s not very helpful. Our error message tells us nothing about what caused the error. Was it something in our request? Was it from our function? Well in this case we actually know, because we purposely threw and error in the function, but what happens in the case where we don’t know what caused the error?

The reason for the response is because of how API Gateway and Lambda work together. All API Gateway sees is that an error occurred in the Lambda function, and simply returned a generic error message. This is fine except in the cases where we want to return more detail, like was it a bad request, does the requested resource exist and so on.

API Gateway wants us to return a response, whether it’s a success or not, we can’t simply throw an error back to API Gateway.

So let’s look at how API Gateway DOES like it’s responses from Lambda. Well it’s quite simple, as JSON!

By default Lambda gives you some code to help you along your way. Your handler function should look something like:

exports.handler = async (event) => {
    // TODO implement
    const response = {
        statusCode: 200,
        body: JSON.stringify('Hello from Lambda!'),
    };
     return response;
};

From this we can see that we can set a status code for our response, which represents the HTTP response status code and a body, which must be stringified JSON. Ok perfect! We can use this to help us return errors.

First let’s change the statusCode to a 404, and curl the endpoint, adding a -I to your curl command to see the response headers. You should see something like below:

HTTP/1.1 404 Not Found
Date: Mon, 22 Mar 2021 12:17:05 GMT
Content-Type: text/plain; charset=utf-8
Content-Length: 20
Connection: keep-alive
Apigw-Requestid: abc123

So as you can see, we can easily change the status code when we are sending responses back to a client. In the REST API variation in API Gateway, you would have to setup Method Responses for every HTTP status code you would like to return. With HTTP APIs it’s as simple as just returning the statusCode in your response.

So we can now easily send back HTTP error codes along with an error message.

But this setup is not ideal for two main reasons:

  • We want control over our responses to ensure that we don’t return any sensitive information, like AWS ARNs
  • We want our error responses to follow a consistent structure and a default response for uncaught errors

I will walk through a simple implementation of how to achieve this. Firstly let’s wrap all the code in our handler function with a try/catch, this will allow us to catch any errors thrown in the function and we can then format the errors how we like.

Your handler function should now look like this:

exports.handler = async (event) => {
  try{
    // TODO implement
    const response = {
        statusCode: 200,
        body: JSON.stringify('Hello from Lambda!'),
    };
     return response;
  } catch (_err) {
    console.error(_err);
  }
};

Now let’s create a new Class, that will be used to build instances of our errors. We’ll call it ErrorResponse.

class ErrorResponse {
  constructor(
    message = 'An error occurred',
    statusCode = 500,
  ) {
    const body = JSON.stringify({ message });
    this.statusCode = statusCode;
    this.body = body;
    this.headers = {
      'Content-Type': 'application/json',
    };
  }
}

This helps us structure our error responses, and will allow us to create a default error response for all uncaught errors. When throwing a custom error using the ErrorResponse class that we have created, the first parameter is the message, while the second is the HTTP status code. For example throw new ErrorResponse('User not found’, 404)

Next in our catch block add the following:

  } catch (_err) {
    let err = _err;
    // if error not thrown by us
    if (!(err instanceof ErrorResponse)) {
      console.error(err);
      err = new ErrorResponse();
    }
    return err;
  }

As you can see, in this block we check to see if the error thrown was an is an instance of our ErrorResponse class, if not, we create a new instance, which will then be returned to the client. This will ensure that only our own custom errors are returned.

Next let’s throw an error! In the try block let’s add a line to throw an error. Add something like this just before the return in the handler function:

throw new ErrorResponse('Oh no!);

Perform a curl on the endpoint, and you should see the error being returned, with a 500 HTTP response code.

Let’s update the error to include a different HTTP status code. Update the thrown error to include a 401 error code. Then run the curl command to see the change in the error response.

Congratulations! Now you know how to easily return custom error responses in your serverless APIs using AWS API Gateway and AWS Lambda.

Don’t forget to destroy the AWS resources you created to avoid any potential billing.

SO WHAT DO YOU THINK ?

We love to talk about ideas, innovation, technology and just about anything else. Really, we love to talk.

Contact fathom
a