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.
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.
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.
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()
:
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 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.
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
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.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens while a user interacts with your app. 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. Start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.
One Reply to "Parallelism, concurrency, and async programming in Node.js"
So you are saying that when multiple requests are executed (as they may have different tasks) is concurrency and if in a particular request, multiple parts getting executed parallely is parallelism?