Skip to main content

AWS: Passing errors from Lambda to API Gateway

2022-12-19 Update: In 2023 AWS updated the interface for API Gateway. I've updated this article to reflect the new interface in places where it is materially different from the old version.

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 // highlight-line
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 // highlight-line
response.body = "We referenced a non-existent variable (on purpose)" // highlight-line
context.fail(JSON.stringify(response)) // highlight-line
}

// 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 // highlight-line
response.body = error // highlight-line
throw new Error(JSON.stringify(response)) // highlight-line
}

// 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.

Method Response Setup

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

method response

Set the status code to 400 and then make sure that you add a header name with Access-Control-Allow-Origin. Soon we will add the '*' value to this header, which will enable CORS for this response.

method response header and code

Integration Response Setup

Navigate to Integration Response > Create Response

integration response

And configure the response for the 400 status code.

add integration response

Here are the values for this form:

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

Status Code: 400

Access-Control-Allow-Origin Header: '*'

Mapping Template Type: application/json

Mapping Template Body: $input.path('$.errorMessage')

Regex Explanation

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. You must be very specific with this regex. For example if we threw statusCode:400 instead of status:400 in our Lambda function, this regex would not match it and we would not get a 400 response. You can use a tool like regex101 to test your regex.

Header Value Explanation

The header value is the value that will be appended to the response. In this case we are setting the Access-Control-Allow-Origin header to '*' which will enable CORS for this response from all locations using the wildcard *.

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.

Mapping Template Explanation

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.

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.

Comments

Recent Work

Free desktop AI Chat client, designed for developers and businesses. Unlocks advanced model settings only available in the API. Includes quality of life features like custom syntax highlighting.

Learn More

BidBear

bidbear.io

Bidbear is a report automation tool. It downloads Amazon Seller and Advertising reports, daily, to a private database. It then merges and formats the data into beautiful, on demand, exportable performance reports.

Learn More