DEV Community

Ayako yk
Ayako yk

Posted on

Understanding the Node.js Event Loop and Asynchronous Flow

As mentioned in the previous article, Node.js operates on a single thread and does not create a new thread for each request. Instead, when performing I/O operations, Node.js efficiently waits for these operations to complete and then resumes execution, allowing it to handle thousands of concurrent connections. This is made possible by its asynchronous nature. This article explains how asynchronous programming and the Event Loop work in Node.js.

  1. JavaScript's Single-Threaded Nature
  2. Asynchronous Handling
  3. Event Loop
  4. Node.js and the Event Loop
  5. Phases

JavaScript's Single-Threaded Nature
JavaScript is single-threaded and doesn't natively support parallel execution. On the client side, users interact with web pages by clicking buttons or requesting additional data. Without asynchronous handling, the browser may freeze while waiting for tasks to complete, preventing users from interacting with the page. To address this, callbacks are used, but as code becomes larger and more complex, it can lead to callback hell (nested functions within functions). To solve this, JavaScript provides asynchronous tools like Promises and Async/Await. For more details, I've written about them in a past blog post.

Asynchronous Handling
Execution environments like browsers or Node.js offer APIs for handling asynchronous tasks. Node.js, in particular, uses a non-blocking I/O model that offloads time-consuming operations to the system kernel --- the core of the operating system that bridges hardware and software. The Event Loop manages this process, enabling Node.js to efficiently handle numerous tasks in a single-threaded environment.

Event Loop
The Event Loop is a mechanism that enables JavaScript and Node.js to handle asynchronous tasks within a single-threaded environment.

In a previous blog post, I explained how the Event Loop works in JavaScript within the browser: Understanding JS Execution Flow with Visuals. In Node.js, the concept is similar, but since Node.js is server-side, it uses the libuv library to manage high-performance tasks and includes additional phases to handle I/O operations more effectively.

Differences between JavaScript (Browser) and Node.js
Environment:
JavaScript => Browser
Node.js => Server-Side with libuv

APIs:
JavaScript => WebAPIs
Node.js => Node.js APIs (e.g., fs, http)

Task Types:
JavaScript => Microtasks, Macrotasks
Node.js => Microtasks, Macrotasks, I/O callbacks

Node.js and the Event Loop
When Node.js starts, it initializes the Event Loop, which consists of multiple phases. Each phase processes an FIFO (First In, First Out) queue of callbacks.

Image description

Phases
The following overview is cited from the Node.js documentation.

Timers: this phase executes callbacks scheduled
by setTimeout() and setInterval().

The execution timing is significantly affected by the execution time of other functions.

Pending callbacks: executes I/O callbacks deferred to the next loop iteration.

For example, TCP errors are handled in this phase.

Idle, prepare: only used internally.

Poll: retrieve new I/O events; execute I/O related callbacks (almost all with the exception of close callbacks, the ones scheduled by timers, and setImmediate()); node will block here when appropriate.

The poll phase performs two main tasks:

  • Manages the next task and determines how long it should block.
  • Processes I/O-related tasks.

Once non-blocking tasks (e.g., reading files) are completed, their callbacks are executed in this phase.

  • If the queue has tasks, it processes them in order.
  • If the queue is empty, it calculates the time until the next setTimeout() or setInterval() and waits accordingly.

Check: setImmediate() callbacks are invoked here.

setImmediate() differs from setTimeout() in that it executes its defined function immediately after the poll phase is completed, whereas setTimeout() schedules a callback based on a time threshold that may be affected by the context.

Close callbacks: some close callbacks, e.g. socket.on('close', ...).

This phase handles close events, such as socket.on('close', function). These events are triggered when a socket connection is closed abruptly. For graceful closures, the process may use process.nextTick() instead to handle the cleanup.

process.nextTick()
When a function is passed to process.nextTick(), the Node.js engine schedules it to be executed immediately after the current operation completes, before any I/O events or timers. This is possible because process.nextTick() uses its own queue, separate from the microtask and macrotask queues. Functions scheduled with process.nextTick() are executed before setTimeout() or setImmediate().

Understanding the asynchronous flow and the Event Loop is crucial for writing high-performance and efficient code, especially when using methods like timers and process.nextTick().

Top comments (0)