Node.js: Promise and Async-Await

·

5 min read

Callback Hell

Why do we even need Promise and Async-Await? They allow us to avoid the Callback Hell. When the number of nesting levels of functions increases, it becomes hard to read and manage the code. That pyramid code structure is what we refer to as Callback Hell.

setTimeout(function () {
    console.log('Value 1');
    setTimeout(function () {
        console.log('Value 2');
        setTimeout(function () {
            console.log('Value 3');
            // More nested callbacks...
        }, 1000);
    }, 1000);
}, 1000);

In the above example, "Callback 2" will be printed only after "Callback 1" gets printed. Likewise, "Callback 3" gets printed only after "Callback 2" gets printed. We tend to use callback hell when we need to perform one asynchronous task after another asynchronous task completes.

Promise

We can do the same serial asynchronous tasks easily with the help of Promise without the need to get into callback hell. A Promise is an object that promises to return a result for us. It can either be resolved or rejected. Let's see a pseudo-code structure for using Promise:

new Promise(task1)                       // Create Promise
.then(task2)                         // Runs if Promise resolved
.then(task3)                         // Runs if above 'then()' succeeds
.
. 
.then(task)                          // Runs if above 'then()' succeeds
.catch(error)                        // Runs if Promise rejected
.finally(conclude);                  // Runs at last anyway
  • At first, we create a Promise Object which executes the task1 callback(Asynchronous Task).

  • If task1 succeeds, then the Promise is resolved else rejected.

  • The task2 callback of then(task2) gets executed if the Promise gets resolved.

  • Now, then(task2) also returns a Promise that gets resolved on success.

  • then(task3) gets exeucted if then(task2) is resolved and so on.

  • If any of the Promise gets rejected, then the error callback inside catch(error) gets executed.

  • The conclude callback of finally(conclude) is executed at last for conclusion.

You can see multiple then() after another then(). It’s called Promise Chaining. So far so good!

Let's implement the Promise in a real code.

// Create Promise
const myPromise = new Promise((res, rej) => {
    setTimeout(() => res('Value 1'), 1000);
})
.then(value1 => {
    console.log(value1);
    return new Promise((res, rej) => {
        setTimeout(() => res('Value 2'), 1000);
    });
})
.then(value2 => {
    console.log(value2);
    return new Promise((res, rej) => {
        setTimeout(() => res('Value 3'), 1000);
    });
})
.then(value3 => {
    console.log(value3);
})
.catch(err => {
    console.log(err);
});

Here, we created a Promise object with a task callback that has two parameters res and rej. We run res when we want the Promise to get resolved after a certain operation. Whereas, we run rej when we want the Promise to get rejected.

  • The first Promise executes its callback and returns a response of 'Value 1' after 1000 milliseconds.

  • The next then() method captures that 'Value 1' and prints it. This then() method also returns another Promise with a response of 'Value 2' after 1000 milliseconds.

  • The next then() method captures the 'Value 2' and prints it. It also returns another Promise with a 'Value 3' response.

  • Again, the next then() method captures 'Value 3' and prints it.

In this way, one asynchronous task can be performed after another in sequential order.

Did you find the Promise syntax overwhelming?

Say hi to Async-Await

This also allows us to avoid the callback hell but, has a relatively easier syntax. We can only use the async-await concept inside an asynchronous function. Let's see a pseudocode structure of it:

async function do_something()
{
    await new Promise(task1);
    await new Promise(task2);
}

Here, instead of using the then() method, we just write the await keyword before the Promise and that's it!

  • The first Promise will make the asynchronous function 'do_something()' freeze until it is resolved/rejected. Other Promises don't get the chance to execute until this Promise is resolved/rejected.

  • Then, the second Promise will also make the entire function wait until it is resolved/rejected.

That seems like a piece of cake. Now, if you want to use the value returned by the Promise, then it can simply be assigned in a variable like this:

async function do_something()
{
    let value1 = await new Promise(task1);
    let value2 = await new Promise(task2);
}

What if you get errors along the way? You can always use the good old try-catch for error handling as follows:

async function do_something()
{
    try
    {
        let value1 = await new Promise(task1);
        let value2 = await new Promise(task2);
    }
    catch(err)
    {
        console.log(err);
    }
}

Now, let's see an example of using the async-await with setTimeout:

async function do_something(){
    try
    {
        // Wait for a Promise to resolve
        let value1 = await new Promise((res, rej) => {
            setTimeout(() => res('Value 1'), 1000);
        });
        console.log(value1);
        // Wait for a Promise to resolve
        let value2 = await new Promise((res, rej) => {
            setTimeout(() => res('Value 2'), 1000);
        });
        console.log(value2);
    }
    catch(err)
    {
        console.log(err);
    }
}
do_something();
  • Here, the first Promise gets resolved after 1000 milliseconds, till then no other code from 'do_something()' gets executed.

  • Then the value1 gets printed and so on.

💡
NOTE: The await keyword only freezes the asynchronous wrapper function 'do_something()' when a specific Promise is being waited for. Any code outside the asynchronous function is allowed to execute during that phase.

Let's see another example of the async-await for clarity:

async function do_something(){
    try
    {
        // Wait for a Promise to resolve
        let value1 = await new Promise((res, rej) => {
            setTimeout(() => res('Value 1'), 1000);
        });
        console.log(value1);
        // Wait for a Promise to resolve
        let value2 = await new Promise((res, rej) => {
            setTimeout(() => res('Value 2'), 1000);
        });
        console.log(value2);
        // Wait for a Promise to resolve
        let value3 = await new Promise((res, rej) => {
            setTimeout(() => res('Value 3'), 1000);
        });
        console.log(value3);
    }
    catch(err)
    {
        console.log(err);
    }
}
do_something();

// Async-Await of do_something() method don't care about this code 
setTimeout(() => 
{
    console.log("Outside Code");
}, 1500);

The output of this code is:

Value 1
Outside Code
Value 2
Value 3

Notice, that the "Outside Code" got executed independently without waiting for do_something() because the async-await has nothing to do with the code outside its wrapper function.

Congratulations, you made it to the end. Go listen to some music.