Asynchronous Javascript
Callbacks are the functions that are passed as an argument to another function and they are executed after some operation has been completed. callbacks are used to handle asynchronous operations.
function add(callback){
let a=5;
let b=10;
callback(a,b);
}
add((a,b) => {
console.log("sum :",a+b);
})
Callback Hell :
Callback Hell occurs when a callback is passed inside a callback function like when multiple nested callbacks are used in asynchronous JavaScript making it difficult to read. And the code looks like “pyramid” or “triangulat” shape.
setTimeout(() => {
console.log("Task 1");
setTimeout(() => {
console.log("Task 2");
setTimeout(() => {
console.log("Task 3");
setTimeout(() => {
console.log("Task 4");
}, 1000);
}, 1000);
}, 1000);
}, 1000);
Each task is dependent on previous task and that resulting in nested code, which makes it harder to read and follow. Callback hell can be avoided using techniques like “Promises” or “async/await” which provide more readable and maintainable solutions.
Promises:
Promise is an object that represents an eventual completion or rejection of an asyncronous operation.
A promise is created by using the ‘Promise’ constructor and it requires two arguments ‘resolve’ and ‘reject’. The ‘resolve’ function is called when the asynchronous task completes successfully, while ‘reject’ is called when there’s an error.
And we attach the .then() method to the promise to log out the message when it resolves successfully, and to handle the errors we need to attach the .catch() method when promise rejects.
If we want to attach .finally() method that represents either promise is resolve or reject but we log some message in it. In Promises the task executes only once.
Now let’s see an example of promise.
let promise=new Promise((resolve,reject) => {
setTimeout(() => {
resolve("resolved");
},2000);
})
promise.then((message) => {
console.log(message);
})
.catch((err) => {
console.error(err);
})
.finally(() => {
console.log("Task completed");
})
Promise Chaining:
A Promise chain refers to an asynchronous operations that are executed one after the other, where each operation depends on the result of the previous one.Each .then() method can return a value that will be passed to the next .then()
. This avoids deeply nested callback hell. If any promise in the chain is rejected, then the control passes to the nearest .catch() method to handle the errors.
Now let’s an example of promise chain:
let promise=new Promise((resolve,reject) => {
resolve(1);
})
promise.then((value) => {
console.log(value);
return value*2;
})
.then((value) => {
console.log(value);
return value*3;
})
.then((value) => {
console.log(value);
return value*4;
})
.then((value) => {
console.log(value);
})
.catch((err) => {
console.error(err);
})
Advantages of Promises:
It avoids the callback hell and makes code easier to read and maintain
It Provides better error handling by using a single .catch() method to handle errors instead of managing them at each level of callbacks.
Promise Methods:
JavaScript provides several methods to work with promises, Now let’s see the methods.
1.Promise.all()
It takes multiple promises as an array and it resolves only when all the promises are resolved. If any one of the promise is rejected, the Promise.all() method rejects. It will very useful when we want to make sure that multiple promises are resolved.
Let’s see an example:
let promise1=new Promise((resolve,reject) => {
setTimeout(() => {
resolve(10);
},1000);
})
let promise2=new Promise((resolve,reject) => {
setTimeout(() => {
resolve(20);
},2000);
})
let promise3=new Promise((resolve,reject) => {
setTimeout(() => {
resolve(30);
},1500);
})
Promise.all([promise1, promise2, promise3])
.then((results) => {
console.log(results);
})
.catch((err) => {
console.error(err);
})
The above example log out the array of promises, because all promises are resolved.
let promise1=new Promise((resolve,reject) => {
setTimeout(() => {
resolve(10);
},1000);
})
let promise2=new Promise((resolve,reject) => {
setTimeout(() => {
reject(20);
},2000);
})
let promise3=new Promise((resolve,reject) => {
setTimeout(() => {
resolve(30);
},1500);
})
Promise.all([promise1, promise2, promise3])
.then((results) => {
console.log(results);
})
.catch((err) => {
console.error(err);
})
above example witll throw an error, beacuse one of the promise was rejected.
2.Promise.race()
Promise.race() resolves or rejects as soon as one of the promises completes, regardless of the outcome of the others. Simply which promise is first executed first either it is resolves or rejects, and that will only gives.
Let’s see an example:
let promise1=new Promise((resolve,reject) => {
setTimeout(() => {
reject(20);
},2000);
})
let promise2=new Promise((resolve,reject) => {
setTimeout(() => {
resolve(30);
},1500);
})
Promise.race([promise1, promise2])
.then((result) => {
console.log(result);
})
.catch((err) => {
console.error(err);
});
In above example the Promise.race() method will logs the output 30, because it resolves first than the other promises.
3.Promise.any()
Promise.any() resolves as soon as any one of the promises gets resolves, and it ignores the rejections. It only checks if any one of the promise gets resolved first and that will be resolves.
let promise1=new Promise((resolve,reject) => {
setTimeout(() => {
resolve(10);
},3000);
})
let promise2=new Promise((resolve,reject) => {
setTimeout(() => {
reject(20);
},2000);
})
let promise3=new Promise((resolve,reject) => {
setTimeout(() => {
resolve(30);
},1500);
})
Promise.any([promise1, promise2,promise3])
.then((result) => {
console.log(result);
})
.catch((err) => {
console.error(err);
});
In above example the Promise.race() method resolves the value 30, beacuse it resolves first than the other promises.
4.Promise.allSettled()
The Promise.allSettled waits for all the promises to either resolve or reject, but it never fails. Instead of stopping at the first rejection, it gives you the results of all the promises, whether they resolved or rejected.
Let’s see an example:
let promise1=new Promise((resolve,reject) => {
setTimeout(() => {
resolve(10);
},3000);
})
let promise2=new Promise((resolve,reject) => {
setTimeout(() => {
reject(20);
},2000);
})
let promise3=new Promise((resolve,reject) => {
setTimeout(() => {
resolve(30);
},1500);
})
Promise.allSettled([promise1, promise2, promise3])
.then((results) => {
console.log(results);
});
It gives the output as shown below,
[ { status: 'fulfilled', value: 10 },
{ status: 'rejected', reason: 20 },
{ status: 'fulfilled', value: 30 } ]
async and await:
But JavaScript didn’t stop there. With the introduction of ‘async’ and ‘await’ in ES8(2017), writing an asynchronous code became even simpler and more readable.
The main advantage of async and await is, there’s no need to chain .then() methods. You can write asynchronous code that looks and behaves like traditional synchronous code, improving readability and maintainability.
The async keyword is used before the function keyword and is used to define an asynchronous function. It always returns a Promise, even if the function itself doesn’t return one. When you use async, it allows the function to handle asynchronous operations while still look like a synchronous code.
The await keyword can only be used inside an async function. It pauses the execution of the function and wait for the Promise to settle (either it resolved or rejected). This allows you to write an asynchronous code in a more readable way.
Let’s see an example how the async and await work together:
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Data fetched successfully");
}, 2000);
});
}
async function displayData() {
console.log("Fetching data...");
const result = await fetchData();
console.log(result);
}
displayData();
In above example, fetchData() function simulates an API call that will take 2 seconds. The displayData() function is an async function, allowing it to use await inside. When the code reaches await fetchData(), it waits for the function execution until fetchData() resolves.
Once the data is fetched, the function continues and logs the result.
Now let’s how to handle the errors by using try and catch method in async functions
Handling Errors with try and catch:
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Data fetched successfully");
}, 2000);
});
}
async function displayData() {
try{
console.log("Fetching data...");
const result = await fetchData();
console.log(result);
}
catch(err){
console.error("Error ",err);
}
}
displayData();
Example with Multiple await Statements:
async function fetchData1() {
return new Promise((resolve) => {
setTimeout(() => {
resolve("Data 1")
},1000)
});
}
async function fetchData2() {
return new Promise((resolve) => {
setTimeout(() => {
resolve("Data 2")
},2000)
});
}
async function displayMultipleData() {
try{
console.log("Fetching data...");
const data1 = await fetchData1();
console.log(data1);
const data2 = await fetchData2();
console.log(data2);
}
catch(err){
console.error("Error: ",err)
}
}
displayMultipleData();
In above example, it waits for the first async function until the promise resolves, and then logs the result and again it waits for the second async function until the promise resolves, and then logs the result.
Subscribe to my newsletter
Read articles from Mohan Chandu directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by