AWS: Passing errors from Lambda to API Gateway

Alternate Resources

This article by Kenn Brodhagen is the best article I have found on this topic:

How to return a custom error object and status code from API Gateway with Lambda

Here are some others:

AWS Docs: Handle Lambda errors in API Gateway

AWS Blog: Error Handling Patterns in Amazon API Gateway and AWS Lambda

dev.classmethod.jp: How to map lambda error to API Gateway error response with Serverless Framework

Intro

One of the commonly annoying things about a serverless workflow with API Gateway and Lambda is that if you trigger a Lambda function with API Gateway and the function fails, you will still get a status code of 200 on the request.

status 200 on error

Because as far as API Gateway is concerned, it did it’s job, it got your request through the Gateway and called the function. You will only get non 200 responses from API Gateway if your authentication fails, a mapping error, or some other error that happens within API Gateway.

This is not very useful for catching errors in your client however. So to fix this we just have to do a little bit of extra mapping magic in API Gateway and make sure we are throwing our errors properly in Lambda.

Throwing the Error

The reason we are throwing an error in Lambda is because this generates a JSON document that looks like this:

{
    "errorType": "ReferenceError",
    "errorMessage": "x is not defined",
    "trace": [
      "ReferenceError: x is not defined",
      "    at Runtime.exports.handler (/var/task/index.js:2:3)",
      "    at Runtime.handleOnce (/var/runtime/Runtime.js:63:25)",
      "    at process._tickCallback (internal/process/next_tick.js:68:7)"
    ]
  }

Specifically note the key “errorMessage”. Later in this article we are going to tell API Gateway to look for this key in the response, and if it finds it to throw a 400 response. Throwing an error is how we generate this error object with the “errorMessage” key.

Here is an example where we set the status of the response to 400. Will this give us a 400 error code in the browser?

exports.handler = async event => {
  let response = {
    status: 200,
    body: undefined,
    headers: {
      "Content-Type": "application/json",
    },
  }

  try {
    // purposely throw error
    console.log(variableThatDoesntExist)
    response.body = "This will be skipped"
  } catch (error) {
    response.status = 400
    response.body = "We referenced a non-existent variable (on purpose)"
  }

  //  dynamic response
  return response
}

If we run this function the status code of the API Gateway response is still 200, because we have to tell API Gateway the specific conditions we want for it to throw a 400 code (IE “errorMessage” key is in Lambda response).

response code 200

Let’s throw an error.

exports.handler = async (event, context) => {
  let response = {
    status: 200,
    body: undefined,
    headers: {
      "Content-Type": "application/json",
    },
  }

  try {
    // purposely throw error
    console.log(variableThatDoesntExist)

    response.body = "This will be skipped"
  } catch (error) {
    response.status = 400
    response.body = "We referenced a non-existent variable (on purpose)"
    context.fail(JSON.stringify(response))
  }

  //  dynamic response
  return response
}

or you could use

exports.handler = async event => {
  let response = {
    status: 200,
    body: undefined,
    headers: {
      "Content-Type": "application/json",
    },
  }

  try {
    // purposely throw error
    console.log(variableThatDoesntExist)

    response.body = "This will be skipped"
  } catch (error) {
    response.status = 400
    response.body = error
    throw new Error(JSON.stringify(response))
  }

  //  dynamic response
  return response
}

etc etc… there are many ways to throw an error.

Throwing an error generates the “errorMessage” object key in the Lambda response, which we then configure API Gateway to look for below. It is possible to throw a 400 response without this by telling API gateway to look for another key in the response, but for the sake of having a reliable and reproducible method documented, we are throwing an error.

Create Method Response

Moving over to API Gateway now, start by picking whichever method you are working with and navigate to Method Response > Add Response

method response

add response

Add a 400 status (or whatever) and then head over to the integration response.

Mapping the Error

Lastly we need to do two things. We need to create a very simple mapping template, and add a Regex string that will trigger the 400 code we just created.

Navigate to Integration Response > Add Integration Response

integration response

add integration response

The regex that is used here is a special java format called Pattern.

The Regex string is looking for matches in the output and if it finds them it will throw the corresponding status code that it matched. We will use the following.

Lambda Error Regex: .*"status":400.*

and then we select the 400 response status that we created in the previous step.

Lastly we need to create a very short mapping template that will tell API Gateway where we are looking for this regex match.

Mapping_Template
$input.path('$.errorMessage')

mapping template

We are looking inside the errorMessage object, which is the object that is created when you throw the error in Lambda.

Response Mapping Recap

I just want to be very clear about what we are doing here. We are instructing API Gateway to look inside the “errorMessage” key if it exists in the response, and then if it see’s status:400 inside the “errorMessage” it will throw a 400 code response. Status 400 is inside the errorMessage because we manually set it that way inside the catch block of our Lambda function. If your Lambda failing returns something different, you can configure API Gateway to look for that instead. We are showing one pattern that works here, but you can change this to suit your needs. If your Lambda does not throw an error, and you are not returning a status:400 key inside that error, these specific response mapping settings won’t work for you.

CORS Headers

There is actually one last step that we need to handle here. If you are experiencing CORS errors in the client it is probably because you are missing the “Access-Control-Allow-Origin” header which is typically appended by API Gateway to a 200 response. One of the things that happens when we take manual control of the 400 responses is that we have manually control the headers. The default 4xx headers that are configured in API Gateway will no longer apply to this route. Therefore we need to manually set the Access-Control-Allow-Origin header in the response to prevent an error in the browser.

Add a header in the method response.

method response header

And then define the value in the integration response.

integration response header

Conclusion

And there you have it, if we run the function again and get an error we will get the following

correct status code

This was a very simple implementation. You can create more complicated regex that can capture a wider range of items from the error message object and there are examples of that elsewhere. But hopefully now the basic idea is clear.