Implementing asynchronous operations in javascript / typescript?
“Think of a kitchen with multiple chefs. In synchronous programming, each chef would take turns using the same kitchen to cook their dishes. One chef would start cooking, and no other chef could start cooking until the first chef finished. In asynchronous programming, each chef would have their own kitchen and would be able to cook multiple dishes in parallel.“
Asynchronous programming is very similar! . This solution works perfectly when the chef gets the orders properly propagated from the customer. But let’s say due to a high demand and crowd at the restaurant if the orders get mixed up, a customer may end up getting pasta instead of pizza :).
This is exactly what happened in our async API implementation. This issue manifested itself by how we passed our objects by reference in our async functions. In this article, I’d like to share what we learned so that others can have a head start working through asynchronous APIs.
Let’s have a look at this problem with a simple example below.
Problem:
The following is an example of a handler function for an API operation.
async function sampleHandler(context) {
(async () => {
await new Promise(resolve => setTimeout(resolve, 1000));
console.log(context.request.body.name)
})();
context.response.code = 200
}
// A sample context object
const sampleContext = {
request: {
body: {
name: 'Alice'
}
},
response: {}
}
sampleHandler(sampleContext)
// Mutating the context object by modifying the request body
sampleContext.request.body.name = 'Bob'
sampleHandler(sampleContext)
To Run Click Here
The output of the above program is like this
Bob
Bob
Instead of
Alice
Bob
In the above example, the sampleHandler API async function returns an HTTP 200 sync response and prints the request body afterwards via an anonymous internal async function.
The above example is with two simultaneous async function calls. The same issue occurs if there are simultaneous requests to the API server and the API library passes a mutated context object instead of a fresh one. In that case with the above example handler function may print the wrong body for a different request. This is because the context object is being passed by reference to the async function while it is mutated elsewhere.
Solution:
We can solve this problem by creating a deep copy of the required data from the context passing it down to the async function as per below.
async function sampleHandler(context) {
const name = context.request.body.name;
(async () => {
await new Promise(resolve => setTimeout(resolve, 1000));
console.log(name)
})();
context.response.code = 200
}
// A sample context object
const sampleContext = {
request: {
body: {
name: 'Alice'
}
},
response: {}
}
sampleHandler(sampleContext)
// Mutating the context object by modifying the request body
sampleContext.request.body.name = 'Bob'
sampleHandler(sampleContext)
To Run Click Here
If you need to pass down an object, you can use spread operator (…), although it is not a deep copy and copies the parameters in first level but it should be sufficient for most cases. We may use some expensive deep copying functions but at the cost of some resource utilization.
Takeaways
We ran into this situation when using the “Koa” API library which was solved by using the above method. Hopefully the information I’ve shared here will be useful and being aware of the common pitfalls can save you significant time while implementing / debugging the asynchronous APIs.