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
  • When researching a particular issue, how do people usually take notes? Like documents found, images, links, notes...

    I often research a specific topic. For example, if I come across an interesting image, I save it to my computer; documents are similar, and links are saved in my browser's Bookmarks... But when I try to find them later, I have no idea where everything I saved is, or how to search for it. Sometimes I even forget everything I've done before, and when I look back, it feels like it's all brand new 😃.

    So I'm nurturing a plan to build a storage space for everything I learn, not just for myself but also with the hope of sharing it with others. This would be a place to contain research topics, each consisting of many interconnected notes that create a complete notebook. Easy to follow, easy to write, and easy to look up...

    I write a blog, and the challenge of writing lies in the writing style and the content I want to convey. Poor writing can hinder the reader, and convoluted content can strip the soul from the piece. Many writers want to add side information to reinforce understanding, but this inadvertently makes the writing long-winded, rambling, and unfocused on the main content.

    Notebooks are created to address this issue. There's no need for overly polished writing; instead, focus on the research process, expressed through multiple short articles linked to each other. Additionally, related documents can also be saved.

    That’s the plan; I know many of you have your own note-taking methods. Therefore, I hope to receive insights from everyone. Thank you.

    » Read more
  • altcha-org/altcha is an open-source project that serves as an alternative to reCaptcha or hCaptcha.

    Studying these projects is quite interesting, as it allows you to learn how they work and how they prevent "spam" behavior 🤓

    » Read more
  • Manus has officially opened its doors to all users. For those who don't know, this is a reporting tool (making waves) similar to OpenAI's Deep Research. Each day, you get 300 free Credits for research. Each research session consumes Credits depending on the complexity of the request. Oh, and they seem to have a program giving away free Credits. I personally saw 2000 when I logged in.

    I tried it out and compared it with the same command I used before on Deep Research, and the content was completely different. Manus reports more like writing essays compared to OpenAI, which uses bullet points and tables.

    Oh, after signing up, you have to enter your phone number for verification; if there's an error, just wait until the next day and try again.

    » 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

The secret stack of Blog

As a developer, are you curious about the technology secrets or the technical debts of this blog? All secrets will be revealed in the article below. What are you waiting for, click now!

As a developer, are you curious about the technology secrets or the technical debts of this blog? All secrets will be revealed in the article below. What are you waiting for, click 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...