Exploring the Future of Serverless with Sheen Brisals: Why It's Becoming the New Norm
Asynchronous programming plays a crucial role in a developer’s life… As a developer, we might have all dealt with asynchronous programming at least once. So when we google what is Javascript, we get results saying “JavaScript is a single-threaded, non-blocking, asynchronous, concurrent programming language…” So Javascript is single-threaded as well as asynchronous together with lots of flexibility. So it might be a little confusing how it can be asynchronous as it is single threaded. If we understand how the execution stack (call stack) in Javascript works, we will understand how asynchronous programming works in Javascript. By default in Javascript, every line in our code or function execute sequentially ie, one line at a time. The Javascript engine always maintains a stack called execution stack to track the current function in execution. When a function is invoked, it gets pushed into the stack and once it finishes its execution, it gets popped out of the stack and when we invoke the next function, the same process happens. This is the synchronous part of Javascript.
How does Javascript work asynchronously?
Now think, we have to invoke an api in one of our function:
function callApi () {
console.log(“Inside callApi function”);
// Call api
console.log(“Api called successfully”);
}
function anotherFunction () {
console.log(“Inside another function”);
}
anotherFunction();
callApi();
anotherFunction();
What do you expect from the output of the above code?
The result will be :
// Inside another function
// Inside callApi function
// Inside another function
// Api called successfully
So this is how asynchronous Javascript works. Basically, we don’t have to wait for a long-running task to finish to continue the sequential tasks. So here another stack called callback stack other than call stack is maintained.
We can perform asynchronous programming in Javascript in different ways. Promises are one of the ways to perform asynchronous tasks.
What are promises?
As MDN docs say:
Asynchronous programming is a technique that enables your program to start a potentially long-running task, and then rather than having to wait until that task has finished, to be able to continue to be responsive to other events while the task runs. Once the task is completed, your program is presented with the result
Understanding Promises in Javascript
Promises are one way to deal with asynchronous code, without getting stuck in callback hell. It lets you know whether the asynchronous function completes successfully or failed. A promise is basically an object in Javascript which has 3 states:
- Pending
- Resolved
- Rejected
So when we are dealing with asynchronous programming, we invoke the function, wait for the function to be completed (PENDING state) and it either completes successfully ie gets resolved or failed ie, gets rejected.
How to use Promise?
Create Promise object using the constructor :
const promise = new Promise((resolve, reject) => {
//Condition to resolve or reject
});
An executor function is passed to the constructor which takes two parameters resolve and reject. The executor function gets invoked automatically and performs the job. It will have a condition to check whether the promise is resolved or rejected.
const promise = new Promise((resolve, reject) => {
// Pending state
if (some condition) resolve(‘Promise is resolved successfully’)
else reject (‘Promise is rejected’)
});
Consumers:
As we have read, promises can be either resolved or rejected. Based on this we can consume the results using the methods .then, .catch, and .finally.
.then
When a promise gets resolved, the .then method is invoked. It takes a function as argument.
promise.then((response) => console.log(response)); // output : Promise is resolved successfully
Here we just console the response from the promise.
.catch
When a promise gets rejected, the .catch method is invoked. It is similar to .then . It also takes a function as argument. We can say it is mainly used for error handling.
promise.catch((error) => console.log(error)); // output : Promise is rejected
.finally
The handler is called when the promise is settled, whether fulfilled or rejected.
Orchestrating Promises
There might be situation when we have to synchronise multiple promises at a time. Promise.all helps as to define a list of promises, and execute something when they are all resolved.
const asyncTaskOne = () => {
return new Promise((resolve, reject) => {
setTimeOut(() => {
if(condition) resolve(‘Promise 1 is resolved’);
else reject(‘Promise is rejected’);
}, 2000)
})
}
const asyncTaskTwo = () => {
return new Promise((resolve, reject) => {
setTimeOut(() => {
if(condition) resolve(‘Promise 2 is resolved’);
else reject(‘Promise is rejected’);
}, 1000)
})
}
const allPromises = [asyncTaskOne(), asyncTaskTwo()];
console.log(allPromises) // [Promise { “pending”}, Promise {“pending”}]
Now we just need to pass the array allPromises to Promise.all.
Promise.all(allPromises)
.then((response) => console.log(response)) //[“Promise 1 is resolved”, “Promise 2 is resolved”]
.catch((error) => console.log(error));
As we can see here, when all the promises get resolved, we get the results. But the downside of this is if one promise gets rejected then the rest of the promises also fail. Then Promise.all get rejected. To handle this:
const allPromises = [asyncTaskOne().catch(e => e), asyncTaskTwo().catch(e => e)];
Use case of Promises
Recently I was working on a project and I was building functionality that requires updating of n number of items in AWS DynamoDB. The naive method is to loop through n items and update each item one by one. But the downside of this method is that this process is synchronous and so the DB updation will be performed one at a time and it is time-consuming.
We can perform the update of all items asynchronously using Promise.all.
Consider a DynamoDB table Books and each item in the table are having attributes
- bookID (partition key)
- bookName
- isAvailable
Consider we are having bookID of all the books with isAvailable as false and we have to update isAvailable to true.
We can use AWS lambda to perform the operation.
Function to Update Item in db:
const updateBookItem = (id) => {
return new Promise(async (resolve, reject) => {
try {
const updateParam = {
TableName: ‘Books’,
Key: {
bookId: id
},
UpdateExpression: ‘SET isAvailable = :isAvailable”;
ExpressionAttributeValues: {
‘:isAvailable’: true
}
}
await dynamoDB.update(updateParams).promise();
resolve(`Successfully updated book with id ${id}`);
} catch(err) {
reject(err);
}
}
}
Lambda Handler:
Consider we get the id’s of all books to be updated from the event. // event.bookIds = [1,2,3,4]
const AWS = require(“aws-sdk”);
const dynamoDB = new AWS.DynamoDB.DocumentClient();
exports.lambdaHandler = async (event, context) => {
try {
const { bookIds } = event;
const promises = bookIds.map(eachId => updateBookItem(eachId).catch(e => e));
const updateResponse = await Promise.all(promises);
console.log(updateResponse);
} catch(err) {
console.log(`[ERROR] ${err}`);
}
}
As we can see from the above code, we are making an array of Promises and it is invoked by Promise.all. We have handled the situation where a promise gets rejected so that even if one promise gets rejected, others won’t fail.
We can also use Promise.allSettled(iterable)
to handle the rejection of any of the promises. The response is such a way that it contains the status of each promise
Sample output of Promise.allSettled(iterable)
for the above examples will be like :
[{
status: ‘fulfilled’,
value: “Promise is resolved”
}, {
status: ‘rejected’,
value: “Promise is rejected”
}]
Conclusion
In this blog, we have learned about asynchronous programming in Javascript. Javascript typically implements asynchronous programming using callbacks. But that might result in a huge codebase. In this blog, we have seen that programming asynchronously is made easier by promises. Furthermore, we can use async-await to make it feel as if it were synchronous and for more cleaner and understandable code.