Unraveling JavaScript Promises: A Solution to Callback Hell

20 / Mar / 2024 by Deepesh Agrawal 0 comments

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 and reject.
  • 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:

  1. myPromise.then((data) => { ... }): This part of the code registers a callback function to be executed when the promise is resolved successfully.
  2. .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.

FOUND THIS USEFUL? SHARE IT

Leave a Reply

Your email address will not be published. Required fields are marked *