Node.js: Using Async/Await Function to Avoid Promise Chaining & Callback Hell

This article shows how you can use Async/Await function instead of Callback functions or Promise while writing the synchronous programming code in Node.js.

Node.js is asynchronous/non-blocking in nature. If we want to execute certain code only after the execution of another code then we can use Callback functions. However, there can be a need of using nested callback functions which results in a situation called Callback Hell.

Use of Promise is a good solution to the callback hell problem. The Promise object represents whether an asynchronous operation has been completed (resolved) or failed (rejected) along with that operation value. As compared to callback functions, Promises are good at error handling and readability of the code as well.

However, there can be a situation where you have chained promises. Async/Await simplifies the process of writing chained promises. Async/Await feature is officially rolled out from Node.js version 8 onwards.

– Async/Await is built on top of promises.
– It is non-blocking (just like promises and callbacks).
– Async functions return a Promise.
    – If the function throws an error, the Promise will be rejected.
    – If the function returns a value, the Promise will be resolved.
– The functions need not be chained one after another.
    – The function simply await the function that returns the Promise.
    – async needs to be declared before awaiting a function returning a Promise.

Previously, I have written some articles on Node.js asynchronous nature, using callback functions, and using Promise:

1. Node.js: Asynchronous & Synchronous Code Programming
2. Node.js: Using Callback Functions for Synchronous Programming
3. Node.js: Using Promise to Avoid Callback Hell

Let’s get started with Async/Await function.

You simply put async word infront of the function name.


// Normal function
function sum(x, y) {
    return x + y;
}

// Async function
async function sum(x, y) {
    return x + y;
}

// Async function with await expression
async function test() {
    let promise = new Promise(function(resolve, reject) {
        setTimeout(function() {
            resolve('done!');
        }, 1000);
    });

    let result = await promise; // wait till the promise resolves

    console.log(result);
}

– An async function can contain an await expression.
– Await expression pauses the execution of the async function and waits for the passed Promise’s resolution.
– After the Promise resolution, it resumes the async function’s execution and returns the resolved value.
– So, we can say that the await operator is used to wait for a Promise.
– Await keyword is only valid inside the async function.
– If you use the await keyword outside of the async function then you will get a SyntaxError.

EXAMPLE 1: SIMPLE EXAMPLE WITH TIMEOUT

In the example below, we will:

– look into the asynchronous code without using callback, promise, or async/await
– then we use callback function in it
– then we use promise in it
– then we use async/await in it
– callback function, promise and async/await are used separately

  • Open terminal
  • Go to your Node.js application directory
  • In my case it is:

cd ~/Documents/nodejs

mukesh:nodejs chapagain$ pwd
/Users/mukeshchapagain/Documents/nodejs
  • Create a file app.js and add the following code to it

app.js


function getUserData(userId) {
    setTimeout(function() { 
        console.log('USER ID: ' + userId);
        let userName = 'Mukesh Chapagain';
        return userName;
    }, 1000); // 1000 miliseconds = 1 second
}

function getCountryData(userId) {
    setTimeout(function() { 
        let countryData = 'Kathmandu, Nepal';
        return countryData;
    }, 1000); // 1000 miliseconds = 1 second
}

console.log('Start');
let userId = 2;
let userData = getUserData(userId);
let countryData = getCountryData(userId);
console.log(userData);
console.log(countryData);
console.log('End');
  • Run the application

node app.js

Output:


mukesh:nodejs chapagain$ node app.js
Start
undefined
undefined
End
USER ID: 2

– You can see the undefined value.
– It’s because there is a delay of 1 second each in the getUserData() and getCountryData() function.
– So, the code console.log(userData) executes before the getUserData() function returns the value.
– Also, the code console.log(countryData) executes before the getCountryData() function returns the value.

Using Callback function

We will now use the callback function in the example code above so that we can get the value from the getUserData() and getCountryData() function and print the return value.

app.js


function getUserData(userId, callback) {
    setTimeout(function() { 
        console.log('USER ID: ' + userId);
        let userName = 'Mukesh Chapagain';
        return callback(userName);
    }, 1000); // 1000 miliseconds = 1 second
}

function getCountryData(userId, callback) {
    setTimeout(function() { 
        let countryData = 'Kathmandu, Nepal';
        return callback(countryData);
    }, 1000); // 1000 miliseconds = 1 second
}

console.log('Start');
let userId = 2;
getUserData(userId, function(resultUser) {
    console.log('Printing user data');
    console.log(resultUser);
    getCountryData(userId, function(resultCountry) {
        console.log('Printing country data');
        console.log(resultCountry);
        console.log('End');
    });
});
  • Run the application

node app.js

Output:


mukesh:nodejs chapagain$ node app.js
Start
USER ID: 2
Printing user data
Mukesh Chapagain
Printing country data
Kathmandu, Nepal
End

Using Promise

We will now use Promise in the same example code above so that we can get the value from the getUserData() and getCountryData() function and print the return value.

app.js


function getUserData(userId) {
    return new Promise(function(resolve, reject) {
        setTimeout(function() { 
            console.log('USER ID: ' + userId);
            let userName = 'Mukesh Chapagain';
            resolve(userName); // resolve or reject is used instead of return
        }, 1000); // 1000 miliseconds = 1 second
    }); 
}

function getCountryData(userId) {
    return new Promise(function(resolve, reject) {
        setTimeout(function() { 
            let countryData = 'Kathmandu, Nepal';
            resolve(countryData); // resolve or reject is used instead of return
        }, 1000); // 1000 miliseconds = 1 second
    }); 
}

console.log('Start');
let userId = 2;
let userData = getUserData(userId);
userData.then(function(resultUser) {
    console.log('Printing user value');
    console.log(resultUser);
    
    let countryData = getCountryData(userId);
    countryData.then(function(resultCountry) {
        console.log('Printing country value');
        console.log(resultCountry);
        console.log('End');
    });
}).catch(function(error) { 
    console.log(error); 
});
  • Run the application

node app.js

Output:


mukesh:nodejs chapagain$ node app.js
Start
USER ID: 2
Printing user value
Mukesh Chapagain
Printing country value
Kathmandu, Nepal
End

Using Async/Await

We will now use Async/Await in the same example code above so that we can get the value from the getUserData() and getCountryData() function and print the return value.

app.js


function getUserData(userId) {
    return new Promise(function(resolve, reject) {
        setTimeout(function() { 
            console.log('USER ID: ' + userId);
            let userName = 'Mukesh Chapagain';
            resolve(userName); // resolve or reject is used instead of return
        }, 1000); // 1000 miliseconds = 1 second
    }); 
}

function getCountryData(userId) {
    return new Promise(function(resolve, reject) {
        setTimeout(function() { 
            let countryData = 'Kathmandu, Nepal';
            resolve(countryData); // resolve or reject is used instead of return
        }, 1000); // 1000 miliseconds = 1 second
    }); 
}

async function printData(userId) {
    console.log('Start');

    try {
        let userData = await getUserData(userId);
        console.log(userData);
    } catch(error) {
        console.log(error);
    }
    
    try {
        let countryData = await getCountryData(userId);
        console.log(countryData);
    } catch(error) {
        console.log(error);
    }   

    console.log('End');
}

// Handling rejected Promise without try/catch block
/* async function printData(userId) {
    console.log('Start');

    let userData = await getUserData(userId).catch((err) => { console.log(err); });
    console.log(userData);

    let countryData = await getCountryData(userId).catch((err) => { console.log(err); });
    console.log(countryData);

    console.log('End');
} */

let userId = 2;
printData(userId);
  • Run the application

node app.js

Output:


mukesh:nodejs chapagain$ node app.js
Start
USER ID: 2
Printing user value
Mukesh Chapagain
Printing country value
Kathmandu, Nepal
End

EXAMPLE 2: ASYNCHRONOUS FILE READ

Now, let’s see an example of multiple file read. We will try to read multiple files in Node.js and print the content of those files.

  • Open terminal
  • Go to your Node.js application directory
  • In my case it is:

cd ~/Documents/nodejs

mukesh:nodejs chapagain$ pwd
/Users/mukeshchapagain/Documents/nodejs
  • Create three text files with some content in them.

mukesh:nodejs chapagain$ echo "Data from File 1" >> myfile_1.txt
mukesh:nodejs chapagain$ echo "Data from File 2" >> myfile_2.txt
mukesh:nodejs chapagain$ echo "Data from File 3" >> myfile_3.txt
  • Create a file app.js and add the following code in it

app.js


const fs = require('fs');

function read(fileName) {
    fs.readFile(fileName, 'utf-8', function(err, data) {
        if (err) { throw err; }     
        return data;
    });
}

console.log('Start');
console.log(read('myfile_1.txt'));
console.log(read('myfile_2.txt'));
console.log(read('myfile_3.txt'));
console.log('End');
  • Run the application

node app.js

Output:


mukesh:nodejs chapagain$ node app.js
Start
undefined
undefined
undefined
End

We get undefined data from all of the files we are trying to read because of the asynchronous nature of Node.js code.

We will solve this issue in three ways:

  • Using Callback functions
  • Using Promise
  • Using Async/Await

Using Callback Function

We will use Callback functions to read multiple files and print their respective content/data.

app.js


const fs = require('fs');

function read(fileName, callback) {
    fs.readFile(fileName, 'utf-8', function(err, data) {
        if (err) { throw err; }     
        return callback(data);
    });
}

read('myfile_1.txt', function(data) {
    console.log('Start \n');
    console.log(data);
    read('myfile_2.txt', function(data) {
        console.log(data);
        read('myfile_3.txt', function(data) {
            console.log(data);
            console.log('End');
        })
    });
});
  • Run the application

node app.js

Output:


mukesh:nodejs chapagain$ node app.js
Start

Data from File 1

Data from File 2

Data from File 3

End

Using Promise

Using Promise is a better way to handle this issue as compared to using of Callback functions. So, here we will use Promise to read multiple files and print their respective content/data.

app.js


const fs = require('fs');

function read(fileName) {
    return new Promise(function(resolve, reject) {
        fs.readFile(fileName, 'utf-8', function(err, data) {
            if (err) { 
                reject(err);
            } else {
                resolve(data);
            }           
        });
    }); 
}

console.log('Start \n');

let dataFirst = read('myfile_1.txt');
dataFirst.then(function(resultFirst) {
    console.log(resultFirst);

    let dataSecond = read('myfile_2.txt');
    dataSecond.then(function(resultSecond) {
        console.log(resultSecond);

        let dataThird = read('myfile_3.txt');
        dataThird.then(function(resultThird) {
            console.log(resultThird);
            console.log('End');
        }).catch(function(error) { console.log(error) });

    }).catch(function(error) { console.log(error) });

}).catch(function(error) { console.log(error) });
  • Run the application

node app.js

Output:


mukesh:nodejs chapagain$ node app.js
Start

Data from File 1

Data from File 2

Data from File 3

End

Using All Promise at once

Instead of running the file read operation each time individually and using .then() every time, we can use Promise.all(iterable) method to execute all the nested promises at once and get the final result as an array of all individual promise result.

– An array of functions can be passed as an argument to Promise.all method.
– The Promise.all(iterable) method returns a single Promise that resolves when all of the promises in the iterable argument have resolved.
– It rejects with the reason of the first promise that rejects.

Here’s the basic syntax of Promise.all(iterable) method:


Promise.all([promise1, promise2, promise3]).then(function(values) {
  console.log(values);
});

We can use it in our file read operation like below:

app.js


const fs = require('fs');

function read(fileName) {
    return new Promise(function(resolve, reject) {
        fs.readFile(fileName, 'utf-8', function(err, data) {
            if (err) { 
                reject(err);
            } else {
                resolve(data);
            }           
        });
    }); 
}

console.log('Start \n');

Promise.all([
    read('myfile_1.txt'), 
    read('myfile_2.txt'), 
    read('myfile_3.txt')
]).then(function(values) {
  console.log(values);
  console.log('\nEnd');
});
  • Run the application

node app.js

Output:


mukesh:nodejs chapagain$ node app.js
Start

[ 'Data from File 1\n',
  'Data from File 1\n',
  'Data from File 1\n' ]

End

Using Async/Await

We use Async/Await feature to address the issue of Promise chaining or nested promises. So, here we will use Async/Await to read multiple files and print their respective content/data.

Note that,

– the read() function should return a Promise
– we will create a new async function to print out the read file content data

app.js


const fs = require('fs');

function read(fileName) {
    return new Promise(function(resolve, reject) {
        fs.readFile(fileName, 'utf-8', function(err, data) {
            if (err) { 
                reject(err);
            } else {
                resolve(data);
            }           
        });
    }); 
}

async function printData() {
    console.log('Start \n');

    try {
        let dataFirst = await read('myfile_1.txt');
        console.log(dataFirst);
    } catch (error) {
        console.log(error);
    }

    try {
        let dataSecond = await read('myfile_2.txt');
        console.log(dataSecond);
    } catch (error) {
        console.log(error);
    }

    try {
        let dataThird = await read('myfile_3.txt');
        console.log(dataThird);
    } catch (error) {
        console.log(error);
    }

    console.log('End');
}

printData();
  • Run the application

node app.js

Output:


mukesh:nodejs chapagain$ node app.js
Start

Data from File 1

Data from File 2

Data from File 3

End

Async/Await also supports Promise.all() so that we don’t have to wait for each promise in case of multiple promises like in the file read example above.

app.js


const fs = require('fs');

function read(fileName) {
    return new Promise(function(resolve, reject) {
        fs.readFile(fileName, 'utf-8', function(err, data) {
            if (err) { 
                reject(err);
            } else {
                resolve(data);
            }           
        });
    }); 
}

async function printData() {
    console.log('Start \n');

    try {
        let results = await Promise.all([
            read('myfile_1.txt'), 
            read('myfile_2.txt'), 
            read('myfile_3.txt')
        ]);

        console.log(results);
    } catch (error) {
        console.log(error);
    }

    console.log('End');
}

printData();

Output:


mukesh:nodejs chapagain$ node app.js
Start

[ 'Data from File 1\n',
  'Data from File 2\n',
  'Data from File 3\n' ]

End

Hope this helps. Thanks.