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.
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).
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
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.
Integration Response Setup
Navigate to Integration Response > Create Response
And configure the response for the 400 status code.
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.
$input.path('$.errorMessage')
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
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
Basalt
basalt.softwareFree 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.
BidBear
bidbear.ioBidbear 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.