In the previous article, Node.js animated: Event Loop, you explored how Libuv and the event loop enable asynchronous task handling in Node.js' single-threaded environment. We simplified the event loop as a mechanism that pushes callbacks from a single event queue to an empty call stack. In reality, the event loop is composed of multiple phases, each responsible for specific asynchronous tasks.
A phase is a FIFO queue of callbacks to execute. When the event loop enters a given phase, it executes callbacks until the queue is exhausted or the maximum number of callbacks is run and moves to the next stage.
The event loop has 6 phases, and they run in the following order:
- Timers: executes callbacks scheduled by setTimeout() and setInterval().
- Pending callbacks: executes I/O callbacks deferred to the next loop iteration.
- Idle-prepare: only used internally.
- Poll: retrieve new I/O events and execute I/O-related callbacks
- Check: executes callbacks scheduled by setImmediate().
- Close callbacks: some close callbacks, e.g., socket.on('close', …).
The event loop doesn’t constantly keep spinning. If there are no active I/O handlers and the event loop doesn’t need to process any callbacks, the Node.js program automatically exits. On the other hand, the event loop stops on the poll phase to capture incoming requests whenever you create a web server and the phases are empty.
You are ready to apply your newly acquired knowledge. Still, you can be disappointed when running a
setTimeout after a
setImmediate, or vice versa, because the result is not deterministic.
setTimeout schedules a callback after at least the defined milliseconds have elapsed. The CPU could be busy and Libuv is unable to set the task as complete before the event loop processes the timer phase. Consequently, the setImmediate callback could be run in advance, although the timer is the first phase.
In this example, the CPU is not busy, and the
setTimeout callback is immediately added to its dedicated queue before
In this second example, the event loop starts processing the timer phase, but the timer has not expired yet, so setImmediate callback is run in advance.
You are now wondering how I can test the phases processing order. The following code snippet will help you prove the order of the phases.
Once Node.js starts the program, the event loop processes all the queues and blocks on the poll phase waiting for incoming requests. When someone makes a GET HTTP request to the server, the event loop goes through these steps:
- The event loop pushes the request handler on the call stack.
setImmediatecallback is added in the check phase.
setTimeoutcallback is scheduled in the timers phase.
- The event loop moves to the check phase. The
setImmediatecallback is popped off from the queue and pushed on the call stack for execution.
- The event loop does not move any callbacks from empty queues.
setTimeoutcallback is popped off from the timer queue and pushed on the call stack for processing.
The code is available at:
Now, you understand the event loop and its phases. You can also tell the difference between
setImmediate and expect a non-deterministic behavior when running them in sequence. In addition, you know that event loop blocks on the poll phase waiting for new I/O requests on a Node.js web server.
But where are promises handled in Node.js?
You will explore it in more detail in the following articles.
- Node.js official documentation: The Node.js Event Loop, Timers, and process.nextTick()
- Libuv event loop source code