WittCode💻

What is the Node Event Loop?

By

Learn what the Node event loop is and how it works. We will go over the different phases of the event loop, micro and macro queues, and the C library Libuv.

Table of Contents 📖

Event Loop

When a Node application is running, there is an event loop running. When a Node application stops, so does the event loop. This event loop orchestrates the execution of both synchronous and asynchronous operations. Consider the code below:

const http = require('http');

console.log('I am synchronous!');
http.get('http://example.com', (err, data) => {
  console.log('I am asynchronous!');
});
console.log('I am synchronous too!');

Running the code above will result in the following output:

I am synchronous!
I am synchronous too!
I am asynchronous!

Notice how the synchronous tasks are executed instantly while the asynchronous tasks are delayed. This is because the event loop executes synchronouos tasks one at a time while it offloads asynchronous tasks to the system's kernel.

INFO: Because most kernels are multithreaded, they are capable of handling multiple tasks at the same time.

Between each run of the event loop, Node will check to see if there are any ongoing asynchronous tasks. If there aren't, Node shuts down cleanly causing the event loop to shut down as well. If there are still onging tasks, when the kernel finishes the task, it uses a callback function to notify the event loop that the task has completed.

Libuv

When we talk about offloading asynchronous tasks to the system, we need to mention Libuv, a very important part of Node's overall architecture. Node consists of several external dependencies to function properly. One of these is Libuv, a C library that focuses on asynchronous I/O. Simply put, Libuv handles Node's asynchronous operations. Libuv was actually primarily developed for use by Node but it is used by other languages too.

INFO: In the browser, JavaScript offloads asynchronous operations to the webapis. On the server, Node offloads asynchronous operations to Libuv.

When an asynchronous task starts, Libuv takes over using the system's kernel. The way Libuv handles these tasks depends on the task itself. For asynchronous tasks like network requests, Libuv relies on the OS primitives while for asynchronous operations like reading from a file, Libuv uses its thread pool.

WARNING: Offloading an asynchronous task to other threads ensures that the main thread is not blocked and can handle other tasks.

Event Loop Phases

The event loop enters different phases. These phases are associated with different tasks. The tasks in these phases are executed in first in first out order (FIFO).

// 1. timers - Executes callbacks scheduled by the setTimeout and setInterval functions.
setTimeout(() => console.log('setTimeout says hello!'), 1000);
setInterval(() => console.log('setInterval says hello!'), 1000);

// 2. pending callbacks - Executes I/O callbacks deferred to the next loop iteration

// 3. idle, prepare - Used internally by Node

// 4. poll - Executes callbacks for I/O operations

// 5. check - Executes callbacks for setImmediate
setImmediate(() => console.log('setImmediate says hello!'));

// 6. close callbacks - Handles close callbacks like closing sockets
socket.on('close')

// 7. process.nextTick - Executes callbacks scheduled by the process.nextTick function

// 8. Native promises - Executes callbacks for native promises

WARNING: A tick is a full round trip of the event loop. The function passed to process.nextTick() is invoked before the next tick starts.

These phases are further separated into both micro and macro task queues. Micro tasks have a higher priority and consist of native promises and the nextTick phase. The others are grouped into macro tasks. Micro tasks are not executed by Libuv while the macro tasks are. Nevertheless, the loop will run until all the callbacks for the program have been processed. If there are more callbacks to be processed, the loop will run again.

WARNING: The micro task queue is attached to each tick in the event loop. When promises are resolved or rejected the microtask is added to the queue for the current tick