JavaScript Callbacks, Promises, and Async/Await

JavaScript is single-threaded but provides some convenient asynchronous mechanisms to provide non-blocking behavior via EventLoop concepts.
As JavaScript developers, we often have a need to deal with code that has one task depending on another task. We can achieve this in two ways synchronous programming and asynchronous programming.
Synchronous
Perform one task at a time and when the task is completed we move to another task. The important fact here is it is Blocking i.e. the thread need to wait for the task to finish the current one to move to the next.
The below code snippet executes the statements one after the other — nothing fancy.
function printMe(msg){
console.log(msg);
}
function getUser(){
return {name: "Tom"};
}
printMe("init");
printMe(getUser());
printMe("end");
//Output
init
{name: "Tom"}
end
However, what if the “getUser” function takes 2 seconds to execute, then you’re blocking the event loop to execute further tasks.
Thanks to JavaScript for not providing a Sleep function.
Asynchronous
Allows to perform the tasks without blocking the main thread or running in parallel, the main thread moves to another task before the previous one finishes. The important fact here it is Non-Blocking i.e. the thread need not wait for the task to finish the current one to move to the next.
Example: The below code snippet executes the statement “getUser” asynchronously despite the timeout time being 0 millis i.e. it didn’t wait for getUser to finish to print the end statement.
function printMe(msg){
console.log(msg);
}
function getUser(){
setTimeout(() => {
printMe({name: "Tom"});
},0);
}
printMe("init");
getUser();
printMe("end");
//Output
init
end
{name: "Tom"}
JavaScript enables you to perform async execution in 3 ways
- Callbacks
- Promises
- Async/Awit
Callbacks
What if in the above example you want to print the user name after you receive a response from the function — Callbacks is one way..
The callback is a function that is passed as an argument to another function. This allows a function to call another function, then a callback function can run after another function is finished.
function printMe(msg){
console.log(msg);
}
function getUser(callback){
setTimeout(() => {
callback({name: "Tom"});
},0);
}
printMe("init");
getUser((userObj) => printMe(userObj.name)); //the argument inside getUser is the callback function
printMe("end");
//Output
init
end
Tom
Callback Hell
Let's say after we load the user, we need to load the address, order history, and other details. You will end up with a nesting of functions that leads to a concept called as callback hell. It makes the code very difficult to read, understand and maintain.
function printMe(msg){
console.log(msg);
}
function getUser(callback){
setTimeout(() => {
callback({id:123,name: "Tom"});
},1000);
}
function getAddress(id, callback){
setTimeout(() => {
callback({address: "121, Some street, Some City, Some Country"});
},1000);
}
function getOrders(id, callback){
setTimeout(() => {
callback([{orderId: "124", name:"2 Pizza"}]);
},1000);
}
printMe("init");
//Callback hell :)
getUser((userObj) => {
console.log("loaded user")
printMe(userObj.name)
getAddress(userObj.id, (addressObj) =>{
console.log("loaded address")
printMe(addressObj.address);
getOrders(userObj.id, (ordersObj) =>{
console.log("loaded address")
printMe(ordersObj);
})
})
});
printMe("end");
You can observe in the above code snippet how unreadable is the callback hell.
We can write better code with Promises.. Lets understand it.
Promises
Thanks to ES6 for introducing Promises. A promise is a placeholder for a value that can either resolve or reject at some time in the future. A Promise is an object that contains a status and a value.

Promise Status can contain
- Fulfilled: The promise is resolved or executed successfully
- Rejected: Something went wrong and it is rejected
- Pending: Neither fulfilled nor rejected and it is still pending.
Lets understand this with an example
new Promise((resolve, reject) => {
try{
console.log("init");
resolve({name:"Tom"});
}catch(e){
reject(new Error(e));
}
});
The promise is executed with a Fulfilled state and the object within PromiseResult. (resolve, reject) are constructor arguments to the Promise object that enable you to callback resolution or rejection.

How do we use promises in our code?
There are some built-in functions that enable us to get promise value, and catch exceptions.
- then(): This is called when the promise is moved to fulfilled state.
- catch(): This is called when the promise is rejected.
- finally(): Called when the promise is fulfilled or rejected.
Let's re-write the code using Promises.
function printMe(msg){
console.log(msg);
}
function getUser(){
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("loaded user")
resolve({id:123,name: "Tom"});
},1000);
});
}
function getAddress(id){
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("loaded address")
resolve({address: "121, Some street, Some City, Some Country"});
},1000);
});
}
function getOrders(id){
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("loaded orders")
resolve([{orderId: "124", name:"2 Pizza"}]);
},1000);
});
}
printMe("init");
getUser()
.then((userObj) => getAddress(userObj.id))
.then((userObj, addressObj) => getOrders(userObj.id))
.then((ordersObj) => printMe(ordersObj))
.catch((err) => console.log("Error:", err));
printMe("end");
Notice the more readable code.

Though the code is readable, you noticed that we’ve implemented Promise objects in every function, and also there is a bit of heavy then() chaining.
Async/Await will help. keep reading.
Async/Await
Thanks to ES7 for introducing async/await behavior. we can simply create async functions which implicitly return a promise, yes you don’t need to develop promise objects, rather you simply label a function with the “async” keyword.
Adding the “async” keyword before any function becomes a promise. If you add await keyword then you can avoid then() method chaining.
Let's re-write the above code snippet with Async/Await.
function printMe(msg){
console.log(msg);
}
async function getUser(){
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("loaded user")
resolve({id:123,name: "Tom"});
},1000);
});
}
async function getAddress(id){
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("loaded address")
resolve({address: "121, Some street, Some City, Some Country"});
},1000);
});
}
async function getOrders(id){
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("loaded orders")
resolve([{orderId: "124", name:"2 Pizza"}]);
},1000);
});
}
async function executeMe(){
try{
let userObj = await getUser();
let addressObj = await getAddress(userObj.id);
let orders = await getOrders(userObj.id);
console.log("loaded all data");
}catch(err){
console.log("Error:", err)
}
}
printMe("init");
executeMe();
printMe("end");
So the above code snippet, wherever you need to use await, you need to decorate the function with async.
The important point here is that synchronous execution does block the main thread but async/await (or promises) allows writing code as if it is synchronous but it is executed as non-blocking or not blocking the main thread.
Run promises in parallel with one function
Promise.all() is a helper function that accepts an array of promises, this function returns a promise. However, the order of promises is not guaranteed and they execute in parallel.
If your requirement is to handle multiple promises at once then use Promise.all.
function printMe(msg){
console.log(msg);
}
async function getUser(id){
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("loaded user")
resolve({id:123,name: "Tom"});
},1000);
});
}
async function getAddress(id){
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("loaded address")
resolve({address: "121, Some street, Some City, Some Country"});
},1000);
});
}
async function getOrders(id){
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("loaded orders")
resolve([{orderId: "124", name:"2 Pizza"}]);
},1000);
});
}
async function executeMe(){
try{
let userId = 123;
const allPromises = Promise.all([getUser(userId), getAddress(userId), getOrders(userId)]);
await allPromises;
console.log("loaded all data");
}catch(err){
console.log("Error:", err)
}
}
printMe("init");
executeMe();
printMe("end");
Note: Promise.all() would reject as a whole upon one of any of the promises rejected. If you need to get the values of the resolved promises then ES2020 introduced a new helper function called as Promise.allSettled()