Handle AWS lambda error with API gateway integration using Typescript in a clean...
source link: https://www.tuicool.com/articles/vARf6vr
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Error handling is tedious, but you can’t miss it, because it makes your code robust. AWS lambda is a great way to write your code, and its simple nature enables us to do something very interesting, and make your code clean, I previously looked a lot about best practice of error handling in lambda, but either they mentions other AWS services integration or just lambda-API gateway configuration. Today, let’s talk about the code. How to make this part clean. Even more, we will use Typescript.
1. Overview
First, we need to define clean
, to my idea, I think, clean
means it’s pretty easy to grasp the intention of the function, and testing friendly. In traditional imperative programming. I’d like to separate the algorithm into smaller functions, and compose them in a top level function. Then every step should be easy unit testable.
2. Interact with API Gateway from lambda
Lambda is nothing but a plain function, it is nothing to do with the web concept, API Gateway is the layer which interact with the client, so if you want to communicate with your web client, you have to go through the API Gateway layer. And you need to speak its language. Something like this:
interface PlainObject { [key: string]: any; } const buildRequest = (statusCode:number, body:PlainObject) => ({ statusCode, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
With the above function, now you can easily send a JSON back to the client, just use it will the callback
which is the 3rd parameter of your lambda. invoking callback(null, buildRequest(200, {message: 'ok'}))
, will return 200
, and a {message: 'ok'}
to the client.
The body
property must be a string.
3. What is the problem
The above knowledge leads to our first question, if this callback
is your only way to send a meaningful response, then you have to construct your logic in the handler function. Which, seems to be fine? Let’s see a real world example
Let’s see the following lambda function
const createCompany = async (event, context, callback) => { const idToken = getIdToken(event); if (!idToken) { callback(null, buildRequest(400, { message: "no id token" })); return; } const companyName = getCompanyName(idToken); if (!companyName) { callback(null, buildRequest(400, { message: "no company name" })); return; } try { const response = await companyService.create(company) callback(null, buildRequest(200, response)); } catch(err) { callback(null, buildRequest(500, {message: 'server error, please try again later'})) } }
The logic here is simple:
-
get the idToken from the event
- if no token, return error
-
get the company name from the idToken
- if no company name, return error
-
send request to our microservice endpoint
- if 200, return to the client
- if error, return 500
This code is just…barely readable. When you have more complex logic than this, then it will become total disaster. And even each step getIdToken
, getCompanyName
is highly testable, since they either return a value or null, but the createCompany
will be not that good.
4. What about something like this
const createCompany = async (event, context, callback) => { const idToken = getIdToken(event); const companyName = getCompanyName(idToken); const response = await companyService.create(company) return response }
Wow, YES! You might say. But wait, didn’t I just remove all the if
for the error path, and try catch
for the network part, such that it becomes easy to read.
What if I tell you that:
-
inside the
getIdToken
andgetCompanyName
, it will try getting the value, and whenever the value is not there, it will throw an error. -
Samething for that
companyService.create
-
which means in the body of
createCompany
, you just need to compose the happy path. -
And for all the errors,
createCompany
will handle them or return to the client.
With very few codes, we can do this, really!
5. Let’s unify the errors first
First, let’s unify all the error response:
const success = (body: PlainObject) => buildResponse(200, body); const badRequest = (body: PlainObject) => buildResponse(400, body); const internalError = (body: PlainObject) => buildResponse(500, body); const buildResponse = (statusCode: number, body: PlainObject) => ({ statusCode: statusCode, headers: { "Access-Control-Allow-Origin": process.env.ACCESS_CONTROL_ALLOW_ORIGIN, "Access-Control-Allow-Credentials": true, "Content-Type": "application/json" }, body: JSON.stringify(body) });
Now we have success
:200, badRequest
:400, internalError
: 500, pretty good start.
You need to use them all over your logic every time you want to throw, you choose one of the functions.
Let’s throw them, they born to be thrown. xD
6. One function you will how to handle the rest
export const getIdToken = (event: APIGatewayEvent): IdToken=> { let idTokenString // parse, verify, extract; // try get the token from event if (!idTokenString) { throw badRequest({ message: "no id token" }); } return JSON.parse(idTokenString); };
Is this function testable, very:
describe("getIdToken", () => { it("should return an object when everything is ok", () => { expect(getIdToken(correctEvent)).toEqual({ "x-hasura-default-role": "user", "x-hasura-allowed-roles": ["user"], "x-hasura-user-id": "auth0|5d19e6548cda860ccc6523c2" }); }); it("should throw error if no IdToken", () => { expect(()=> getIdToken(badEvent)).toThrowError(); }); });
You have the idea how to refactor the rest two.
7. Now our champion, the higher-order function
export const handleError = ( handler: ( event: APIGatewayProxyEvent, context: Context ) => Promise<PlainObject> | PlainObject ) => async ( event: APIGatewayProxyEvent, context: Context, callback: Callback<APIGatewayProxyResult> ) => { try { const result = await handler(event, context); callback(null, success(result)); } catch (err) { callback(null, err); } };
Let’s digest it.
-
handleError
is a function, which takes a function, and returns a function, which makes itself a higher-order function. -
It takes a function
handler
which will acceptevent, context
like usual, and returns a function which matches the normal handler signature (3 params). - In the body of the returned function, we try catch everything.
-
Any error being thrown inside the
handler
function, will be re-throw, and since we already unified the error response, we just re-throw without any wrapper. -
If no errors, we return
success
8. How to use it
You simply just wrap your createCompany
lambda with it like this:
export const createCompany = handleError((event, context) => { // our previous logic });
Event better, it will give you error if you try to return anything that is not an object if you use TypeScript.
9. The final result
const createCompany = handleError(async (event, context, callback) => { const idToken = getIdToken(event); const companyName = getCompanyName(idToken); const response = await companyService.create(company) return response })
What a result! Now you can just wrap this handlerError
to each of your lambda, then you can make your code clean.
10. End
I think an important concept is you always want to make your top level (which composes all the steps) easier to follow, because every time you refactor or debug, this is where you start.
Hope it helps. Follow me ( albertgao ) on twitter, if you want to hear more about my interesting ideas.
Thanks for reading!
Recommend
-
1
How To Build an API Gateway REST API Using AWS Lambda Proxy Integration?Amazon API Gateway is an AWS service for managing APIs at any scale. Broadly it’s categorized into two products - REST APIs and HTTP APIs. While both allow us...
-
2
Learn How AWS Lambda Annotations Framework Makes API Gateway Integration Easy. Let's learn what the Lambda Annotation Framework aims to solve and how it compares with older ways of building Lambda Functions. We will see th...
-
1
Build a serverless website from scratch using S3, API Gateway, AWS Lambda, Go and Terraform Mar 18, 2019 In this guide we will leverage AWS to build a completely serverless website (frontend and...
-
4
Using .NET AWS Lambda Authorizer To Secure API Gateway REST APIWhen building serverless APIs with AWS Lambda and API Gateway, one of the most critical questions is how to secure the API.Lambda Authorizers are a feature provided by...
-
5
Introduction What is an API? In simple terms, API is a messenger; let’s understand this with some examples. Let’s say you are hungry and you need to cook something at home. If you want to make noodles, you just take the...
-
11
Using Apache NiFi in OpenShift and Anywhere Else to Act as Your Global Integration Gateway What does it look like? Where Can I Run This Magic Engine: Private Cloud, Public Clo...
-
1
-
3
How to Handle Exceptions When Processing DynamoDB Stream Events in .NET Lambda FunctionDynamoDB Streams capture a time-ordered sequence of events in any DynamoDB table.In a previous blog post,
-
5
How to Handle Exceptions When Processing SQS Messages in .NET Lambda FunctionSeptember 22, 2022 -5 min readThis article is sponsored by AWS and is part of my
-
5
How to Handle Numbers Represented as Strings in the Input to a .NET AWS Lambda FunctionDownload
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK