Unraveling JavaScript Promises: A Solution to Callback Hell
Before the widespread adoption of promises, callback hell was a common issue in JavaScript. Callback hell refers to the situation where multiple nested callbacks are used to handle asynchronous operations, leading to code that is difficult to read, understand, and maintain.In callback hell, asynchronous functions would often be nested within each other, resulting in a pyramid-like structure of code. This made the code hard to follow, prone to errors, and challenging to debug or refactor.
Promises were introduced in ECMAScript 6 (ES6) as a solution to this problem. Promises provide a cleaner and more readable way to handle asynchronous operations by representing a value that may be available in the future. Promises allow developers to chain asynchronous operations together using methods like .then()
and .catch()
, which makes the code more linear and easier to understand.
Below is the example of callback hell, later we will see, how to solve this using Promises:
// Function to simulate asynchronous task 1
functiontask1(callbackFn) {
setTimeout(() => {
console.log("1")
callbackFn()
}, 1000);
}
// Function to simulate asynchronous task 2
functiontask2(callback) {
setTimeout(function() {
console.log("Task 2 completed");
callback();
}, 1500);
}
// Function to simulate asynchronous task 3
functiontask3(callback) {
setTimeout(function() {
console.log("Task 3 completed");
callback();
}, 500);
}
// Nested callbacks to execute tasks sequentially
task1(function() {
task2(function() {
task3(function() {
console.log("All tasks completed");
});
});
});
// Output
// Task 1 completed
// Task 2 completed
// Task 3 completed
// All tasks completed
Creating a Promise
const myPromise = new Promise((resolve, reject) => {
// Simulating an asynchronous operation (e.g., fetching data from a server)
setTimeout(() => {
// Assuming the operation was successful
const data = {
message: "Data successfully fetched"
};
// Resolve the promise with the data
resolve(data);
// If the operation fails, reject the promise
// reject(new Error("Failed to fetch data"));
}, 2000); // Simulating a delay of 2 seconds
});
In this example:
- We create a new promise using the
Promise
constructor, which takes a function with two parameters:resolve
andreject
. - Inside the function, we simulate an asynchronous operation using
setTimeout
. This could be any asynchronous task, such as fetching data from a server. - If the operation is successful, we call
resolve
with the data we want to pass - If the operation fails, we call
reject
with an error object
Let’s try consoling myPromise variable, and see what it will show
There are 2 things, one is Promise state and another is result.
Promise state – It tells in which state promise is in, like initially it is in pending state and then either it can be fulfilled or rejected. If fulfilled, it means operation completed successfully, if rejected, then it means the the operation gets failed.
Promise result : It holds the data of the promise
How to consume a Promise
we can consume a promise using the .then()
and .catch()
methods.
myPromise.then((data) => {
console.log("data", data)
}).catch((err) => {
console.log("err", err)
})
Here’s what each part does:
myPromise.then((data) => { ... })
: This part of the code registers a callback function to be executed when the promise is resolved successfully..catch((err) => { ... })
: This part of the code registers a callback function to be executed if the promise is rejected (i.e., an error occurs during its execution).
So, when myPromise
resolves successfully, the first callback function inside .then()
is executed, logging the data to the console. If myPromise
is rejected (i.e., an error occurs), the callback function inside .catch()
is executed, logging the error to the console.
Now, let’s go back to the first example which is of callback hell, and let’s solve it using promises:
// Function to simulate asynchronous task 1
functiontask1(callbackFn) {
setTimeout(() => {
console.log("1")
callbackFn()
}, 1000);
}
// Function to simulate asynchronous task 2
functiontask2(callback) {
setTimeout(function() {
console.log("Task 2 completed");
callback();
}, 1500);
}
// Function to simulate asynchronous task 3
functiontask3(callback) {
setTimeout(function() {
console.log("Task 3 completed");
callback();
}, 500);
}
// Execute tasks sequentially using promises
task1()
.then(() => task2())
.then(() => task3())
.then(() => {
console.log("All tasks completed");
})
.catch((error) => {
console.error("An error occurred:", error);
});
So, that’s how easily we can do it through promises. Now, to simplify it further, we can also use async/await. Async/await is just syntactic sugar for the then
block.
async function executeTasks() {
try {
await task1();
await task2();
await task3();
console.log("All tasks completed");
} catch (error) {
console.error("An error occurred:", error);
}
}
executeTasks();
Defining Promise: A Promise is an object that represents eventual completion(or failure) of async operation.
Conclusion
Both solutions, using the then
block and async/await
, produce the same result as the one provided by the callback function. However, writing with async functions is much easier now. Stay tuned for more such blogs.