Asynchronous Programming in Node.js: Callback, Promises & Async/Await

A program usually executes line-by-line, meaning the function written first will execute first, and then the next function will execute. The problem arises when one function while executing blocks the execution of other functions. There may be some cases where this can be super annoying. For instance, suppose you are using a social media platform, and you want to post a long video, it is obvious it takes some time, now in that time you are not able to do any other task, this is what will happen without asynchronous programming.

If the application you are using implements asynchronous programming, you can able to do other tasks, such as messaging friends, watching other people’s videos, etc while your video is uploading.

In this article, we will understand what is asynchronous programming, its importance, techniques, error handling and best practices in the context of Node.js. So let’s start.

What is Synchronous Programming?

Synchronous programming is a style of writing code where tasks are executed in sequence. Let us understand this with an example.

Example:

Below is a synchronous program for reading a file using the FS module.

const fs = require('fs');

const data = fs.readFileSync('hello.txt', 'utf8');
console.log(data);

// self-invoking function
(() => { console.log("Hello From NodeJS") })();

It is important to understand the above code to understand synchronous programming:

  1. In the first line, we have first imported a module fs, fs have functions to work with files.
  2. In the second line, we have called the readFileSync method of the fs module which is used to read files, and then store this method’s return value into constant data. The file we passed inside this function has the content “Hello From TXT” in it.
  3. In line three, we log the data inside the data constant.
  4. In line four, we have implemented a self-invoking function to log a message, this function is called automatically when executed.

Now, let’s run the program.

Output:

Hello From TXT
Hello From NodeJS

You can see the function readFileSync is executed first, printing the contents of the file, and then the self-invoking function is executed. This is because the readFileSync is a synchronous function, it blocks the execution of the rest of the function until it gets executed. The approach to writing the above program is called synchronous programming.

Now let’s try to rewrite the above code in an asynchronous way.

Introduction to Asynchronous Programming

In Asynchronous programming, one function does not block the execution of another function. It means it is a way of writing the program so that multiple functions can run at a time. The function that takes less time will execute first than the function that takes more time. The faster function does not have to wait for the completion of the slower function which appears before it sequentially. This makes the overall execution faster and non-blocking and gives the power to perform multiple tasks at once. 

Example:

Below is an asynchronous program for reading a file using the FS module.

const fs = require('fs');

fs.readFile('hello.txt', 'utf8', (err, data) => {
    console.log(data);
    }
);

// self-invoking function
(() => { console.log("Hello From NodeJS") })();

This is the same code as above, we have just used an asynchronous approach here. We have used the readFile method which is an asynchronous function, this function does not block the execution of the rest of the code, and we have passed a callback function so that when this function completes its execution the callback executes and we are able to perform the task that can only be done after this function executed.

Let’s run this program.

Output:

Hello From NodeJS
Hello From TXT

See, since in general, the execution of a function reading files takes more time than a simple one-liner function, the self-invoking function executes first without waiting for its prior function to execute and then when the readFile function is done its execution the respective output gets printed.

How to do Asynchronous Programming in Node.js?

In Node.js, doing asynchronous programming is straightforward and easy. Whenever you use any of its modules, you will find that each module has both synchronous and asynchronous methods as we have seen in the above example. Always try to choose an asynchronous method because of its non-blocking nature, which increases performance and speed.

If an asynchronous method does not exist for the operation you want to perform then you can use third-party modules to help you accomplish different types of tasks asynchronously.

There are some techniques for asynchronous programming in Node.js that you will need to use with asynchronous methods for control flow. Let’s see them one by one.

Asynchronous Programming Techniques

There are three main techniques for asynchronous programming in Node.js: callback, promises, and async/await. Let’s see them one by one.

1. Callback

The callback is a function passed as an argument to the asynchronous function. Once the asynchronous function completes its execution, the code inside the callback function will be executed. The “do this, once you’re done doing that” approach is taken by using callbacks in Node.js.

This is very useful when there are some tasks that need to be executed only after the asynchronous function is executed. For example, logging the output returned by it, or implementing serializable operations.

const fs = require('fs');

fs.readFile('hello.txt', 'utf8', (err, data) => {
    console.log(data);
});

2. Promises

Just like the Node.js modules have asynchronous methods, they also have promise-based API methods that return promises. One of the functions is fs.promises.readFile comes under fs. promises API introduced in Node.js 10.0.0. Using these promise-based API methods can be helpful for performing asynchronous programming since they also run asynchronously in a non-blocking manner. Let’s see an example of this method to demote the use of promise.

const fs = require('fs');

fs.promises.readFile('hello.txt', 'utf8')
    .then(data => {
        console.log(data);
    });

3. Async/Await

We can also use these promise-based API methods with Async/Await. Async/Await is a newer concept. We have a dedicated article on Node.js Promises vs Async/Await if you want to read more about it. For now, let’s demonstrate this for doing asynchronous programming.

const fs = require('fs');

async function readFileAsync() {
    const data = await fs.promises.readFile('hello.txt', 'utf8');
    console.log(data);
}

readFileAsync();

Error Handling in Asynchronous Programming 

Error handling is an important part of asynchronous programming. When an asynchronous operation fails, it is important to handle the error properly.

1. Callback

If you have noticed, in the callback function, we have a first argument as err. We can use this err object to check for the error and log it if it occurs by implementing an if/else block. Let’s see how.

const fs = require('fs');

fs.readFile('hello.txt', 'utf8', (err, data) => {
    if (err) {
        console.error(err);
        return;
    }
    console.log(data);
});

2. Promises

We can chain .catch() method for handling errors with .then() methods when working with promises. Below is an example demonstrating this.

const fs = require('fs');


fs.promises.readFile('hello.txt', 'utf8')
    .then(data => {
        console.log(data);
    })
    .catch(err => {
        console.error(err);
    });

3. Async/Await

With async/await, we can use a try/catch block for error handling. If there is no error then only the statement inside the ‘try’ block will execute and only if an error occurs then the statements inside the ‘catch’ block will execute.

const fs = require('fs');


async function readFileAsync() {
    try {
        const data = await fs.promises.readFile('hello.txt', 'utf8');
        console.log(data);
    } catch (err) {
        console.error(err);
    }
}

readFileAsync();

Conclusion

Node.js is a single-threaded event-driven JavaScript runtime environment built upon the V8 engine that uses Javascript for creating the server side. Node.js uses an event loop & a callback queue to efficiently handle multiple asynchronous operations simultaneously. It is best for creating asynchronous APIs, servers, and real-time applications.

In this article, we learned about Node.js asynchronous programming. We also learned about synchronous programming and why it is not as efficient as asynchronous. We have also learned how to write asynchronous code, we have seen some techniques for doing it, and finally, we have seen how to handle errors while doing async operations. We hope that after reading this article you got enough knowledge about it.

Reference

https://stackoverflow.com/questions/44512388/understanding-async-await-on-nodejs

Aditya Gupta
Aditya Gupta
Articles: 109