Node.js Architecture - How does Node.js handle asynchronous tasks?

Node.js Architecture - How does Node.js handle asynchronous tasks?

Daily short news for you
  • These past few days, well not exactly, due to the recent WWDC event, Apple has been the subject of online discussions about where their AI features actually stand. While other companies are diving into bringing AI to their devices and software, Apple seems to be... not too concerned.

    Recently, Apple's researchers suggested that LLM models will "completely collapse in accuracy" when faced with extremely complex problems. By pointing out that reasoning is merely an illusion, many rebuttals to this research emerged immediately. Once again, it raises the question of what Apple is truly thinking regarding AI on their devices.

    I think it's quite simple, Apple seems to be struggling with creating AI for themselves. That is, they are facing difficulties right from the data collection stage for training. They always appear to respect user privacy, so would they really go online to scrape data from everywhere, or "steal" data from users' devices? Surely, they wouldn't want to provide more user data to third parties like OpenAI.

    However, perhaps these challenges will lead them to discover a new direction. If everyone chooses the easy path, who will share the hardships? 😁. Oh, I'm not an "Apple fan," I just use what suits me 🤓.

    » Read more
  • A "sensitive" person to markdown is someone who jumps right in to see what's new when they come across a library that creates a new editor. Milkdown/milkdown is one example.

    Taking a look, it seems quite good, everyone. I might try integrating it into opennotas to see how it goes. It's supposed to be a note-taking application that supports markdown, but the library tiptap doesn't seem to want to add markdown support 😩. Using an external library isn't quite satisfactory yet.

    » Read more
  • Everyone using Cloudflare Worker to call OpenAI's API should be careful, I've encountered the error unsupported_country_region_territory these past few days. It's likely that the Worker server is calling from a region that OpenAI does not support.

    It's strange because this error has only occurred recently 🤔

    » Read more

Problem

In the previous article, we learned that JavaScript/Node.js has only one main thread to execute JavaScript code. We also understood the difference between synchronous and asynchronous I/O tasks. JavaScript/Node.js smartly adopts an asynchronous handling mechanism to avoid blocking the call stack. In today's article, we will explore how Node.js handles asynchronous operations.

First, let’s get acquainted with a new component: Libuv.

Libuv

Let’s revisit the example from the previous section.

fs.readFile(file.pdf)
  .then(pdf => console.log("pdf size", pdf.size));

fs.readFile(file.doc)
  .then(doc => console.log("doc size", doc.size));

The order in which functions are pushed onto the call stack is as follows.

+------------------------------+
|                              |
| fs.readFile(file.pdf)        |
+------------------------------+
|        Call stack            |
|------------------------------|
             |
             v
+------------------------------+
|                              |
| fs.readFile(file.doc)        |
+------------------------------+
|        Call stack            |
|------------------------------|

Since the results of asynchronous functions are not returned immediately, the call stack executes the two commands above very quickly. So, where and how are the results of these two functions handled? In other words, where does the code inside then go?

Libuv is where asynchronous I/O is handled. Libuv is an external library chosen by Node.js to handle I/O. JavaScript in browsers does not use Libuv but implements something called Web APIs.

When the call stack encounters asynchronous functions, it immediately offloads them to libuv or Web APIs, so it barely does any processing here. Once the asynchronous function has a result, libuv attaches the result to a callback function, as well as the function inside then (note that the callback or then is always a function with a parameter, which is the result returned by libuv), and pushes it into a place called the Callback Queue.

So, what happens to the Callback Queue after libuv has the result?

Event loop

The Event Loop is an essential component of JavaScript/Node.js. It is an infinite loop that never stops while the program is running. The Event Loop's job is to bring callback functions from the Callback Queue back to the call stack.

The Event Loop always monitors the call stack because it only starts transferring callback functions from the Callback Queue to the call stack when the call stack is empty. This is also the reason why the example in the first section prints World before Hello.

setTimeout(function() {
  console.log("Hello");
}, 0);

console.log("World");

setTimeout with a value of 0 has almost no delay, so why is Hello not printed before World? It’s simple: the callback function inside setTimeout was pushed out of the call stack to libuv or Web APIs before the Event Loop could bring it back to the call stack.

The Event Loop has only one job: to bring callback functions from the Callback Queue back to the call stack. It does not directly execute JavaScript code. Code is only executed when it is placed in the call stack. If the call stack is not empty, the functions in the Callback Queue are never brought back to the call stack. This is why it is often said never to block the Event Loop. If blocked, the program becomes sluggish because requests will never have their results returned to the user.

Diagram of Node.js asynchronous handling process

Thanks to the presence of critical components like libuv, Web APIs, the Callback Queue, and the Event Loop, Node.js can process the results of asynchronous functions. This process can be summarized in the following diagram.

+------------------------------+
|         Call Stack           |
|----------------------------- |
| Encounter an asynchronous    |
| function                     |
|                              |
|                              |
| -> Push to libuv/Web APIs    |
+------------------------------+
             |
             v
+------------------------------+
|      libuv/Web APIs          |
|----------------------------- |
| Process I/O                  |
| Once done, push callback     |
| into Callback Queue          |
+------------------------------+
             |
             v
+------------------------------+
|        Callback Queue        |
|----------------------------- |
| [I/O result and callback]    |
|                              |
+------------------------------+
             |
             v
+------------------------------+
|          Event Loop          |
|----------------------------- |
| Check: Is Call Stack empty?  |
| If empty, push callback from |
| Callback Queue to Call Stack |
+------------------------------+
             |
             v
+------------------------------+
|         Call Stack           |
|----------------------------- |
| Execute callback function    |
+------------------------------+

Conclusion

With its single-threaded architecture, Node.js has chosen an asynchronous handling mechanism to optimize performance and prevent the call stack from being blocked by I/O tasks. This mechanism relies on the collaboration of critical components: libuv (a library for asynchronous I/O handling) or Web APIs, the Callback Queue (a queue containing callback functions after I/O completion), and the Event Loop (a loop responsible for bringing callbacks back to the call stack when it is empty). When an asynchronous task is called, Node.js offloads it to libuv or Web APIs for processing. After completion, the result and callback function are placed into the Callback Queue, waiting for the Event Loop to push them onto the call stack for execution. Thanks to this seamless operation, Node.js achieves efficient handling of asynchronous tasks without interrupting the main thread.

In summary, the strength of Node.js in handling asynchronous operations lies in the combination of libuv, the Callback Queue, and the Event Loop. This allows Node.js to maintain high performance but also requires developers to be cautious not to block the Event Loop, which could hinder program optimization. Understanding this mechanism enables us to leverage Node.js more effectively, especially when working with complex I/O tasks.

In the next article, let’s delve deeper into the Event Loop.

Premium
Hello

Me & the desire to "play with words"

Have you tried writing? And then failed or not satisfied? At 2coffee.dev we have had a hard time with writing. Don't be discouraged, because now we have a way to help you. Click to become a member now!

Have you tried writing? And then failed or not satisfied? At 2coffee.dev we have had a hard time with writing. Don't be discouraged, because now we have a way to help you. Click to become a member now!

View all

Subscribe to receive new article notifications

or
* The summary newsletter is sent every 1-2 weeks, cancel anytime.

Comments (0)

Leave a comment...