Event Loop

How Node handles async and non-blocking I/O

2 min read

Execution starts before the event loop

When you run a Node.js program, the entire JavaScript file is executed synchronously from top to bottom. At this stage, there is no event loop running. Functions like setTimeout, file system calls, or network operations do not execute immediately. They only register their intent with Node’s internal systems.


Role of V8 and libuv

Node.js connects two separate systems.
V8 executes JavaScript code and manages the call stack and memory.
libuv interacts with the operating system and manages asynchronous tasks such as timers, file I/O, and network events.

V8 cannot perform I/O, and libuv cannot run JavaScript. Node bridges this gap.


When the event loop begins

The event loop starts only after all synchronous code has finished executing and the call stack is empty. From this point onward, Node continuously checks for pending work and executes callbacks when JavaScript becomes available.


Event loop phases (high level)

The event loop runs in cycles. In each cycle, libuv processes tasks in a defined order:

  • Timers whose delay has expired
  • Completed I/O operations from the operating system
  • Callbacks scheduled with setImmediate
  • Cleanup callbacks for closed resources

This cycle continues as long as there is pending work.


Microtasks and priority

Promises and process.nextTick are not handled by libuv. They are managed by V8 as microtasks. After every callback execution, Node immediately runs all microtasks before continuing the event loop. This is why promise callbacks can run before timers.


Why Node.js scales

Node.js does not execute JavaScript in parallel. Instead, it avoids waiting. Time-consuming or blocking work is delegated to the operating system or to a small internal thread pool, keeping the main JavaScript thread free to handle new events.

This design is the foundation of Node.js performance and scalability.