DEV Community

Matt Eddy
Matt Eddy

Posted on • Updated on

Node.js - Asynchronous Flow

Alt Text

Overview

This article is Part 2 of Working With NodeJS, a series of articles to help simplify learning Node.js. The topic which I will focus on here will be Asynchronous Flow.

Objectives

In this article we'll learn what asynchronous code is, and programming techniques to deal with asynchronous code. I'll also discuss the Event Loop and how it plays a role in managing the application code.

Introduction

The Event Loop runs in a single thread which means that only one task can be executed at time. To alleviate code blocking issues, such as reading a large file, Node.js like JavaScript incorporated a Non Blocking I/O. This means the execution thread does not wait for the operation to complete before moving on to the next task.

Section 1 - Asynchronous Code

Asynchronous code is code that will execute at some point in the future. It can be thought of as code that must notify the Event Loop when it is done so it can be placed on the call stack and executed. Lets see an example.

asynchronous code example
    setTimeout(() => {
        console.log('I\'m still executed last.');
    }, 0);

    console.log('I \'m executed first');
Enter fullscreen mode Exit fullscreen mode

In the code snippet above, even though the setTimeout method delay is 0 the second console.log is executed first. This is because the setTimeout method is a piece of asynchronous code. When the Event Loop encounters the setTimeout method, it places it on the call stack, executes the setTimeout method, and immediately moves on to execute the next line of code. When the timer is finished, the Event Loop is notified and the callback function i.e

() => { 
   console.log('I\'m still executed last.');
 }
Enter fullscreen mode Exit fullscreen mode

is placed at the end of the call stack. At this point the Event Loop has already moved forward to execute the second console.log. Lets look at an example that can be mistaken as asynchronous code.

mistaken as asynchronous code example
let bar;
function asyncFunction(callback) {
    callback();
}

asyncFunction((cb) => {
    console.log('What is bar equal to?', bar);
});

bar = 1;
Enter fullscreen mode Exit fullscreen mode

In the above code snippet, the asyncFunction is not really asynchronous. The callback() gets immediately invoked because it is synchronous. It in fact, goes line-by-line executing each line of code of the asyncFunction. Therefore bar will be undefined.

Callbacks - Programing Technique 1

A callback in Node.js is one way of handling a piece asynchronous code. When the Event Loop encounters asynchronous code that takes a callback, the asynchronous code tells the Event Loop When I'm done I'll call you back. At this point the callback is registered with the Event Loop. It is only when the callback completes the Event Loop is notified there is more code to run. Lets see an example.

readFile code snippet
const { readFile } = require('fs');
readFile(__filename, (err, contents) => {
    if (err) {
        console.error(err);
        return;
    }
    console.log(contents.toString());
});
Enter fullscreen mode Exit fullscreen mode

The readFile method out of the fs module takes a filename and a callback as parameters. Once all the file content is read, the callback will be made to the Event Loop. There are a few other things I would like to point as well.

  1. You cannot do anything with the context of the readFile until the callback is completed. As developers, we tend to try to set some variable to some value in a callback and use it elsewhere within our code. This can be problematic as it could lead to undefined variables because nearly all Node.js API's are asynchronous.
  2. If you define a custom callback function for your asynchronous operation, it is best practice that your first parameter be reserved for the error object. This is because all of Node.js callbacks follow the same protocol.

Promises - Programing Technique 2

A Promise is another way of handling a piece of asynchronous code. Promises provide away to avoid Callback Hell. A Promise can be in one of three states - Pending, Fulfilled, Rejected. The initial state of a promise is pending until the resolve or reject methods are invoked. If you resolve a promise then its in the fulfilled state, and you can consume it using the chainable .then(f) function. If you reject a promise, or an error occurs, It is in the rejected state, and you can use the .catch(f) function to handle these scenarios. Lets see an example.

promise code snippet

new Promise((resolve, reject) => {
    console.log('Initial');
    resolve('A');
})
.then((result) => {
    console.log(result);
})
.then(() => {
    throw new Error('Something failed');
    console.log('This will not execute');
})
.catch((err) => {
    console.error(err);
});
Enter fullscreen mode Exit fullscreen mode
output
Initial
A
Error: Something failed
Enter fullscreen mode Exit fullscreen mode

Lets look at a more practical example. I'll refactor the readFile code snippet from the callback section earlier to return a promise that resolves to the content of a file.

refactored code from callback
const { readFile } = require('fs');

function readFileProm() {
    return new Promise((resolve, reject) => {
        readFile(__filename, (err, content) => {
            if (err) reject(err)
            else resolve(content.toString())
        })
    })
}

const promise = readFileProm();

promise.then((content) => {
    console.log(content);
});

promise.catch((err) => {
    console.error(err);
});
Enter fullscreen mode Exit fullscreen mode

The code snippet above will read its own file content to the console. In Node.js there is a helpful utility function called promisify out of the util module. Promisify takes asynchronous code that is callback based and converts it to promised based asynchronous code. Lets see an example. I'll refactor the code snippet above so that it uses the promisify function. The code will produce the same results.

promisify code snippet

const { promisify } = require('util');
const { readFile } = require('fs');

const readFileProm = promisify(readFile);

const promise = readFileProm(__filename);

promise.then((contents) => {
  console.log(contents.toString());
})

promise.catch((err) => {
  console.error(err);
})
Enter fullscreen mode Exit fullscreen mode

The code snippet above takes a piece of asynchronous code that is callback like readFile, and converts it to asynchronous code that is promise like readFileProm. With promise like asynchronous code we can begin using async and await to block progress within a function until a promise is resolved.

Async and Await - Programing Technique 3

As we learned earlier with asynchronous code, the execution thread will not wait for the function to finish before moving on to the next line of code. Therefore, the only way to obtain a value produced by a piece of asynchronous code is to have nested callbacks or chained then blocks. This is where async and await come into play. Async and await allow us to work with promises by suspending execution of further code until a promise is resolved. Once the promise resolves progress continues within the function. Lets see an example. I'll refactor the promisify code snippet from the promise section so it works with async and await.

async and await code snippet
const { promisify } = require('util');
const { readFile } = require('fs');

const readFileProm = promisify(readFile);

async function read() {
    try {
        const content = await readFileProm(__filename);
        console.log(content.toString());
    } catch (err) {
        console.error(err);
    }
}

read().catch((err) => {
    console.error(err);
});
Enter fullscreen mode Exit fullscreen mode

The code snippet above will produce the same result as the snippet before, but with a cleaner look to the code. In the code snippet above, I created a function read and made it async or asynchronous. This will allow the function to be non blocking and allow other code which is after it to be executed. Within the read function the await keyword is used on the promise like code. At this point, further execution of code is suspended until the promise is resolved.

If you like this post please leave rating and subscribe to the series. Take care.

Top comments (0)