Home > Web Front-end > JS Tutorial > The Node.js Event Loop: A Developer's Guide to Concepts & Code

The Node.js Event Loop: A Developer's Guide to Concepts & Code

Christopher Nolan
Release: 2025-02-12 08:36:12
Original
614 people have browsed it

Asynchronous programming of Node.js: In-depth understanding of event loops

The Node.js Event Loop: A Developer's Guide to Concepts & Code

Asynchronous programming is extremely challenging in any programming language. Concepts such as concurrency, parallelism and deadlock make even the most experienced engineers tricky. Asynchronously executed code is difficult to predict and difficult to track when there is a bug. However, this problem is inevitable, because modern computing has multi-core processors. Each CPU core has its own thermal limit, and the single-core performance improvement has reached a bottleneck. This prompts developers to write efficient code and make full use of hardware resources.

JavaScript is single-threaded, but does this limit the ability of Node.js to take advantage of modern architectures? One of the biggest challenges is dealing with the inherent complexity of multithreading. Creating a new thread and managing context switching between threads is expensive. Both the operating system and programmers need a lot of effort to provide a solution that handles numerous edge cases. This article will explain how Node.js solves this problem through event loops, explores various aspects of Node.js event loops and demonstrates how it works. Event loops are one of the killer features of Node.js because it solves this tricky problem in a completely new way.

Key Points

  • Node.js event loop is a single-threaded, non-blocking, and asynchronous concurrent loop that allows multiple tasks to be processed efficiently without waiting for each task to complete. This makes it possible to process multiple web requests simultaneously.
  • Event loop is semi-infinite, which means it can exit if the call stack or callback queue is empty. This loop is responsible for polling the operating system to get callbacks from incoming connections.
  • Event loop runs in multiple stages: timestamp update, loop activity check, timer execution, pending callback execution, idle handler execution, prepare handle for setImmediate callback execution, calculate polling timeout, blocking I/O , check handle callback execution, close callback execution, and end iteration.
  • Node.js utilizes two main parts: the V8 JavaScript engine and libuv. Network I/O, file I/O, and DNS queries are performed via libuv. The number of threads available for these tasks in the thread pool is limited and can be set through the UV_THREADPOOL_SIZE environment variable.
  • At the end of each stage, the loop executes the process.nextTick() callback, which is not part of the event loop, because it runs at the end of each stage. The setImmediate() callback is part of the entire event loop, so it is not executed immediately as the name implies. It is generally recommended to use setImmediate().

What is an event loop?

Event loop is a single-threaded, non-blocking and asynchronous concurrent loop. For someone without a computer science degree, imagine a web request that performs database lookups. A single thread can only perform one operation at a time. Instead of waiting for the database to respond, it continues to process other tasks in the queue. In the event loop, the main loop expands the call stack and does not wait for the callback. Since the loop does not block, it can handle multiple web requests simultaneously. Multiple requests can be queued at the same time, making them concurrent. The loop does not wait for all operations of a request to complete, but is processed according to the order in which the callback occurs without blocking.

The loop itself is semi-infinite, which means that it can exit the loop if the call stack or callback queue is empty. The call stack can be considered as synchronous code, such as console.log, expanding before looping to poll more work. Node.js uses the underlying libuv to poll the operating system for callbacks from incoming connections.

You may be wondering, why event loops are executed in a single thread? Threads are relatively heavier in memory for the data required for each connection. Threads are operating system resources that need to be started, which cannot be extended to thousands of active connections.

Usually, multithreading can also complicate the situation. If the callback returns data, it must marshal the context back to the thread being executed. Context switching between threads is slow because it must synchronize the current state, such as the call stack or local variables. Event loops can avoid bugs when multiple threads share resources because it is single threaded. Single-threaded loops reduce thread-safe edge cases and enable faster context switching. This is the real genius behind the loop. It effectively utilizes connections and threads while maintaining scalability.

Theory is enough; now let’s see what the code looks like. You can do it in REPL as you like or download the source code.

Semi-infinite loop

The biggest question that event loops must answer is whether the loop is active. If so, determine how long to wait on the callback queue. In each iteration, loop expands the call stack and polls.

This is an example of blocking the main loop:

setTimeout(
  () => console.log('Hi from the callback queue'),
  5000); // 保持循环活动这么长时间

const stopTime = Date.now() + 2000;
while (Date.now() < stopTime) {}
Copy after login
Copy after login
Copy after login

If you run this code, note that the loop is blocked for two seconds. However, the loop remains active until the callback is executed after five seconds. Once the main loop is unblocked, the polling mechanism determines how long it will wait on the callback. This loop ends when the call stack expands and there are no callbacks left.

Callback Queue

Now, what happens when I block the main loop and then schedule the callback? Once the loop is blocked, it does not add more callbacks to the queue:

const stopTime = Date.now() + 2000;
while (Date.now() < stopTime) {}
// 这需要 7 秒才能执行
setTimeout(() => console.log('Ran callback A'), 5000);
Copy after login
Copy after login

This cycle remains active for seven seconds. Event loops are stupid in terms of their simplicity. It has no way of knowing what might be queueing in the future. In actual systems, incoming callbacks are queued and executed when the main loop can be polled. The event loop goes through several stages in sequence when unblocking. So, to stand out in an interview about loops, avoid fancy terms like “event launcher” or “reactor mode”. It is a simple single-threaded loop, concurrent and non-blocking. Event loop using async/await

To avoid blocking the main loop, one idea is to wrap synchronous I/O with async/await:

Anything that appears after await comes from the callback queue. The code looks like a synchronous blocking code, but it doesn't block. Note that async/await makes readFileSync a
const fs = require('fs');
const readFileSync = async (path) => await fs.readFileSync(path);

readFileSync('readme.md').then((data) => console.log(data));
console.log('The event loop continues without blocking...');
Copy after login
Copy after login
thenable

, which removes it from the main loop. Anything that appears after await can be considered as a non-blocking operation through a callback. Full disclosure: The above code is for demonstration purposes only. In actual code, I recommend using fs.readFile, which triggers a callback that can be wrapped around Promise. The overall intent remains valid as this will block I/O removal from the main loop.

Go a step further

What if I told you that event loops are not just call stacks and callback queues? What if an event loop is not just one loop, but multiple loops? What if it could have multiple threads at the bottom?

Now, I want to take you deeper into Node.js.

Event Loop Stage

These are the event loop phases:

The Node.js Event Loop: A Developer's Guide to Concepts & Code

Picture source: libuv Document

  1. Update timestamp. The event loop caches the current time at the start of the loop to avoid frequent time-related system calls. These system calls are internal calls to libuv.
  2. Is the loop active? If the loop has an active handle, an active request, or a closed handle, it is active. As shown, the pending callback in the queue keeps the loop active.
  3. Execute expiration timer. This is where the setTimeout or setInterval callback runs. Loops to check cached now to enable expired active callbacks to execute.
  4. Execute pending callbacks in the queue. If any callbacks were delayed by previous iterations, these callbacks will run at this time. Polling usually runs the I/O callback immediately, with exceptions. This step handles any lagged callbacks from the last iteration.
  5. Execute idle handlers—mainly because of improper naming, because these handlers run in every iteration and are internal handlers for libuv.
  6. Prepare to execute the handle to the setImmediate callback in the loop iteration. These handles run before the loop blocks I/O and prepare a queue for this callback type.
  7. Calculate polling timeout. The loop must know when it blocks I/O. This is how it calculates the timeout:
    • If the loop is about to exit, the timeout is 0.
    • If there is no active handle or request, the timeout is 0.
    • If there are any free handles, the timeout is 0.
    • If there are any pending handles in the queue, the timeout is 0.
    • If there are any handles that are being closed, the timeout is 0.
    • If none of the above is, the timeout is set to the closest timer, and if there are no active timers, it is infinite.
  8. Cycling the duration of the previous stage blocks I/O. I/O-related callbacks in the queue are executed here.
  9. Execute the check handle callback. This stage is the stage of setImmediate running, which is the corresponding stage for preparing the handle. Any setImmediate callback queued during I/O callback execution will run here.
  10. Execute the closed callback. These are the active handles released from the closed connection.
  11. Iteration ends.

You may be wondering why polling blocks I/O when it should be non-blocking? The loop will only block when there are no pending callbacks in the queue and the call stack is empty. In Node.js, the closest timer can be set via setTimeout, for example. If set to infinite, the loop will wait for incoming connections to do more work. This is a semi-infinite loop because polling keeps the loop active when there is no remaining work and there is an active connection.

The following is the Unix version of this timeout calculation, in its entire C code form:

setTimeout(
  () => console.log('Hi from the callback queue'),
  5000); // 保持循环活动这么长时间

const stopTime = Date.now() + 2000;
while (Date.now() < stopTime) {}
Copy after login
Copy after login
Copy after login

You may not be very familiar with C, but this reads like English and does exactly as described in Phase 7.

Phase-by-stage demonstration

To display each stage with pure JavaScript:

const stopTime = Date.now() + 2000;
while (Date.now() < stopTime) {}
// 这需要 7 秒才能执行
setTimeout(() => console.log('Ran callback A'), 5000);
Copy after login
Copy after login

Because the file I/O callback runs before stage 4 and 9, it is expected that setImmediate() will fire first:

const fs = require('fs');
const readFileSync = async (path) => await fs.readFileSync(path);

readFileSync('readme.md').then((data) => console.log(data));
console.log('The event loop continues without blocking...');
Copy after login
Copy after login

Network I/O without DNS queries is less expensive than file I/O because it executes in the main event loop. File I/O is queued through thread pool. DNS queries also use thread pools, so this makes network I/O as expensive as file I/O.

Thread Pool

Node.js has two main parts: the V8 JavaScript engine and libuv. File I/O, DNS query, and network I/O are performed via libuv.

This is the overall structure:

The Node.js Event Loop: A Developer's Guide to Concepts & Code

Picture source: libuv Document

For network I/O, event loops poll within the main thread. This thread is not thread-safe because it does not have context switches with another thread. File I/O and DNS queries are platform-specific, so the method is to run them in a thread pool. One idea is to do DNS queries yourself to avoid entering the thread pool, as shown in the above code. For example, entering an IP address instead of localhost removes the lookup from the pool. The number of threads available in the thread pool is limited and can be set through the UV_THREADPOOL_SIZE environment variable. The default thread pool size is about four.

V8 is executed in a separate loop, clears the call stack, and returns control to the event loop. V8 can use multiple threads for garbage collection outside of its own loop. Think of V8 as an engine that takes the original JavaScript and runs it on the hardware.

For ordinary programmers, JavaScript remains single-threaded because there are no thread safety issues. V8 and libuv internally start their own separate threads to meet their own needs.

If there is a throughput problem in Node.js, start with the main event loop. Check how long it takes for an application to complete a single iteration. It should not exceed one hundred milliseconds. Then, check the thread pool hunger and what can be evicted from the pool. The pool size can also be increased by environment variables. The final step is to perform microbenchmarking of JavaScript code in synchronously executed V8.

Summary

The event loop continues to iterate over each stage because the callback is queued. However, within each stage, there are ways to queue another type of callback.

process.nextTick() and setImmediate()

At the end of each stage, the process.nextTick() callback is executed in a loop. Note that this callback type is not part of the event loop, as it runs at the end of each stage. The setImmediate() callback is part of the entire event loop, so it is not executed immediately as the name implies. Since process.nextTick() requires understanding of the internal mechanism of event loops, I usually recommend using setImmediate().

Several reasons why you may need process.nextTick():

  1. Allow network I/O to handle errors, clean up, or retry requests before the loop continues.
  2. It may be necessary to run the callback after the call stack is expanded but before the loop continues.

For example, an event transmitter wants to trigger an event in its own constructor. The call stack must be expanded before the event can be called.

setTimeout(
  () => console.log('Hi from the callback queue'),
  5000); // 保持循环活动这么长时间

const stopTime = Date.now() + 2000;
while (Date.now() < stopTime) {}
Copy after login
Copy after login
Copy after login

Allowing call stack expansion prevents errors such as RangeError: Maximum call stack size exceeded. One thing to note is to make sure process.nextTick() does not block the event loop. Recursive callback calls within the same stage may cause blocking problems.

Conclusion

Event loop embodies simplicity in its ultimate complexity. It solves a difficult problem such as asynchronicity, thread safety, and concurrency. It removes useless or unwanted parts and maximizes throughput in the most efficient way. Therefore, Node.js programmers can reduce the time to chase asynchronous errors and spend more time on delivering new features.

FAQs about Node.js event loops

What is Node.js event loop? The Node.js event loop is the core mechanism that allows Node.js to perform non-blocking asynchronous operations. It is responsible for handling I/O operations, timers, and callbacks in a single-threaded event-driven environment.

How does Node event loop work? The event loop continuously checks for events or callbacks in the event queue and executes them in the order of addition. It runs in a loop, handling events based on the availability of events, which makes asynchronous programming in Node.js possible.

What is the role of event loops in Node.js applications? Event loops are at the heart of Node.js, which ensures that applications remain responsive and can handle many simultaneous connections without multiple threads.

What are the stages of the Node.js event loop? The event loop in Node.js has several stages, including timer, pending callbacks, idle, polling, checking, and closing. These phases determine how and order the events are processed.

What are the most common event types that are processed by event loops? Common events include I/O operations (for example, reading from a file or issuing a network request), timers (for example, setTimeout and setInterval), and callback functions (for example, callbacks from asynchronous operations).

Node How to handle long-running operations in event loops? Long-running CPU-intensive operations can block the event loop and should be offloaded into child processes or worker threads using modules such as child_process or worker_threads modules.

What is the difference between a call stack and an event loop? The call stack is a data structure that tracks function calls in the current execution context, while the event loop is responsible for managing asynchronous and non-blocking operations. They work together because the event loop schedules the execution of callbacks and I/O operations, and then pushes them to the call stack.

What is the "tick" in the event loop? "tick" refers to a single iteration of the event loop. In each tick, the event loop checks for pending events and executes any callbacks ready to run. Ticks is the basic unit of work in a Node.js application.

The above is the detailed content of The Node.js Event Loop: A Developer's Guide to Concepts & Code. For more information, please follow other related articles on the PHP Chinese website!

Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Latest Articles by Author
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template