Janice Muia I am a full-stack developer based in Nairobi. From time immemorial, I have always enjoyed research and tinkering with technology. I came across software programming during legal research, and since then, I have had the stubborn desire to be a maker and part of the change I would like to see in the world.

Parallelism, concurrency, and async programming in Node.js

4 min read 1150

Parallelism Concurrency Async Node.js

Introduction

Node.js is an an open-source, cross-platform JavaScript runtime built on the Chrome V8 engine. Runtime is the final phase of the program lifecycle during which the program is running as the machine executes the program’s code.

For Node to be effective in runtime, it uses an event-driven, non-blocking I/O model that makes it lightweight and efficient, perfect for data-intensive, real-time applications that run across distributed devices. In this article, we look at the different ways in which the machine will execute code.

Async, concurrency, and parallelism explained

Node.js uses an asynchronous event-driven design pattern, which means that multiple actions are taken at the same time while executing a program. For example, if there are two endpoints that follow one another in the program, the server will move one to make a request to the second endpoint without waiting for the first to return data.

All this is possible thanks to Node’s events mechanism, which helps the server to get a response from the previous API call; this is the asynchronous programming model in a nutshell.

On the other end of the spectrum is synchronous programming, where a single action is taken at a time. This means that as one task is waiting for a response, you cannot call another since the execution of all other parts of the program are blocked from being executed.

Concurrency means that a program is able to run more than one task at a time — this is not to be confused with parallelism. During concurrency, different tasks with differing goals of the program can be performed at the same time, while in parallelism, different parts of the program execute one task.

Concurrency requires proper allocation of resources to work efficiently, while parallelism involves broken bits of the program working independently to achieve the same task. An example of concurrency is the fact that you can download an image while being able to post a reply on the same image.

Parallelism, on the other hand, is achieved through the use of the Web Workers API. Web Workers are probably the only way to achieve “true” multi-processing in JavaScript. We say “true” here because with setInterval(), setTimeOut(), XMLHttpRequest, async/await, and event handlers, we can mimic parallelism.

Callback hell, promises, and handling async execution in Node.js

In synchronous JavaScript, the program executes one task at a time. This means that all other processes are put on hold, which results in a lot of time elapsing between processes. In asynchronous JavaScript, more than one task will be executed at a time, and this creates async callbacks.

Callbacks explained

Async callbacks are functions that are specified as arguments when calling a function, which will start executing code in the background. When the background code finishes running, it calls the callback function to let you know the task is completed. An example is addEventListener():

We made a custom demo for .
No really. Click here to check it out.

btn.addEventListener('click', () => {
  alert('Button clicked!');

  let pElement = document.createElement('p');
  pElement.textContent = 'Hello dear Friend';
  document.body.appendChild(pElem);
});

The first parameter is the type of event to be listened for, and the second parameter is a callback function that is invoked when the event is fired, i.e., the callback function is not executed immediately. It is “called back” later asynchronously somewhere inside the containing function’s body.

The containing function is responsible for executing the callback function when the time comes. Instead of immediately returning some result, like most functions, functions that use callbacks take some time to produce a result.

Asynchronous JavaScript using callbacks isn’t very intuitive, and it can be difficult to get right. Here’s a classic example of so-called callback hell:

fs.readdir(source, function (err, files) {
  if (err) {
    console.log('Error finding files: ' + err)
  } else {
    files.forEach(function (filename, fileIndex) {
      console.log(filename)
      gm(source + filename).size(function (err, values) {
        if (err) {
          console.log('Error identifying file size: ' + err)
        } else {
          console.log(filename + ' : ' + values)
          aspect = (values.width / values.height)
          widths.forEach(function (width, widthIndex) {
            height = Math.round(width / aspect)
            console.log('resizing ' + filename + 'to ' + height + 'x' + height)
            this.resize(width, height).write(dest + 'w' + width + '_' + filename, function(err) {
              if (err) console.log('Error writing file: ' + err)
            })
          }.bind(this))
        }
      })
    })
  }
})

One reason this happens is because programmers may lean towards writing code in such a way that execution happens visually from top to bottom.

Promises to the rescue

Promises help make situations like the chaos above much easier to write, parse, and run. A promise will return a value at some point in the future of a program. For example, due to security and privacy reasons, every time you make a video call, the person on the receiving end is required to grant video access for the call to go through:

function phoneCallButton(evt) {
  setStatusMessage("Ringing...");
  navigator.mediaDevices.getUserMedia({video: true, audio: true})
    .then(chatStream => {
      selfViewElem.srcObject = chatStream;
      callStream.getTracks().forEach(track => myPeerConnection.addTrack(track, callStream));
      setStatusMessage("Connected to user");
    }).catch(err => {
      setStatusMessage("Failed to connect");
    });
}

When the function is called, the status message "Ringing…" will be sent. Thereafter, getUserMedia will be called instead of waiting for the user, getting the chosen devices enabled, and directly returning the MediaStream for the stream created from the selected sources. getUserMedia() returns a promise, which is resolved with the MediaStream once it’s available.

Basically, as long as the app doesn’t know the connection has been made, the phone will keep ringing. Only when it knows whether the phone has been connected or the catch block has been called will it stop.

.then() receives a function with an argument, which is the resolved value of our promise. .catch returns the reject value of our promise. .then() also provides a sort of event queueing, which ensures there is no callback hell.

It is also important to look at async/await functions when it comes to promises. Async functions are declared with the async keyword. The async and await keywords enable asynchronous, promise-based behavior to be written in a cleaner style, avoiding the need to explicitly configure promise chains.

app.post('/user/signin', async (request, reply) => {
        const data = request.body;
        const result = await signinUser(data);
        reply.status(200).send({
            status: 200,
            data: result,
        });
    });

In the above sign-in endpoint, in order to get the result, the function awaits the data. Async functions can contain zero or more await expressions. Await expressions suspend progress through an async function, then resume progress only when an awaited promise-based asynchronous operation is either fulfilled or rejected.

Conclusion

There are various ways to handle async operations in parallel in Node.js. First and foremost, you can use promises, as discussed above.

You can also make use of a third-party library, like the async npm package, which will ensure that you do not end up causing callback hell. You can install it using the following command:

npm install async --save

 

200’s only Monitor failed and slow network requests in production

Deploying a Node-based web app or website is the easy part. Making sure your Node instance continues to serve resources to your app is where things get tougher. If you’re interested in ensuring requests to the backend or third party services are successful, try LogRocket. https://logrocket.com/signup/

LogRocket is like a DVR for web apps, recording literally everything that happens on your site. Instead of guessing why problems happen, you can aggregate and report on problematic network requests to quickly understand the root cause.

LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. .
Janice Muia I am a full-stack developer based in Nairobi. From time immemorial, I have always enjoyed research and tinkering with technology. I came across software programming during legal research, and since then, I have had the stubborn desire to be a maker and part of the change I would like to see in the world.

Leave a Reply