πŸŒ€ Behind the Scenes of Asynchronous JavaScript: The Event Loop in Depth

Gagan BNGagan BN
4 min read

We often hear that the event loop is responsible for handling asynchronous code in JavaScript. While that’s true, today let's dive deeper into how the event loop works internally, especially in the context of Node.js.

πŸ“Œ Overview

The event loop manages the execution of asynchronous operations. Whenever asynchronous code (e.g., setTimeout, fs.readFile, fetch, etc.) is encountered, the work is offloaded:

  • To the browser’s Web APIs (in the case of frontend JavaScript), or

  • To libuv, the underlying library in Node.js that handles asynchronous I/O operations.

Once the synchronous code in the call stack finishes execution, the event loop checks if any asynchronous tasks are ready. If they are, it pushes their corresponding callback functions onto the call stack for execution.

This is the high-level idea, but under the hood, the event loop has multiple phases, each handling specific types of callbacks.


πŸ”„ Event Loop Phases in Node.js

The event loop in Node.js has 4 main phases:

1. Timers Phase

Handles callbacks from:

  • setTimeout

  • setInterval

Note: The timers are not guaranteed to execute exactly after the specified delay. They are queued and executed in this phase only when the stack is clear and the delay has elapsed.

2. Poll Phase

Handles:

  • I/O callbacks such as fs.readFile, net, http, https, crypto, etc.

This is a crucial phase. If the poll queue is empty:

  • And there are no timers due, the event loop waits here for callbacks.

  • If timers are due, it skips the waiting and proceeds directly to the timers phase in the next tick.

3. Check Phase

Handles:

  • setImmediate callbacks

Callbacks scheduled using setImmediate() are executed after the poll phase, giving them a higher priority than timers in some cases.

4. Close Callbacks Phase

Handles:

  • Callbacks like socket.on('close', ...) or process.on('exit', ...)

βš™οΈ Microtasks: The Internal Cycle

Between the main phases of the event loop, there is an internal microtask queue, consisting of two key queues:

1. process.nextTick() Queue

  • Handled before any other microtask

  • Has higher priority than Promises

2. Promise Queue

  • Handles:

    • Promise.then()

    • async/await

    • fetch (on the client side)

Before entering each main phase (Timers, Poll, etc.), Node.js first drains the process.nextTick() queue, followed by the Promise queue. This means these microtasks are executed between each phase of the event loop.


⚠️ Starvation Risk

Using process.nextTick() extensively can lead to starvation, where the event loop never proceeds to the next phase. This happens if:

  • A process.nextTick() callback schedules another process.nextTick()

  • Or deeply nested Promises keep scheduling more microtasks

This can prevent I/O callbacks or timers from executing, leading to unresponsive behavior.

Recommendation: Use process.nextTick() sparingly and prefer setImmediate() or Promises when possible.


🧠 Interesting Node.js Behavior

Once all synchronous and asynchronous code is executed, the event loop enters the Poll phase and waits for new I/O events.

If a callback is ready during this wait (e.g., from a file read), the event loop doesn't restart from the Timers phase. Instead, it continues directly from the Poll phase, where it was waiting.


βœ… Summary

PhaseHandles
TimerssetTimeout, setInterval
PollI/O operations (fs, net, http, etc.)
ChecksetImmediate
CloseClose events (socket.on('close'))

Microtasks (between phases):

  • process.nextTick() (highest priority)

  • Promises (then, async/await)


Understanding the internals of the event loop can help you write better, more predictable, and performant Node.js applications. I hope this gave you a clearer view of what happens behind the scenes!

πŸ§ͺ Code Executions and Their Outputs

Let’s look at a few examples that demonstrate how different asynchronous APIs are scheduled and executed by the event loop.

πŸ“„ Code Sample 1

const a = 100;

setImmediate(() => console.log("setImmediate"));
fs.readFile("./file.txt", "utf8", () => {
  console.log("File Reading CB");
});
setTimeout(() => console.log("Timer expired"), 0);

function printA() {
  console.log("a=", a);
}
printA();

console.log("Last line of the file.");

Output:

a= 100
Last line of the file.
Timer expired
setImmediate
File Reading CB

πŸ“„ Code Sample 2

const a = 100;

setImmediate(() => console.log("setImmediate"));
Promise.resolve().then(() => console.log("Promise"));
fs.readFile("./file.txt", "utf8", () => {
  console.log("File Reading CB");
});
setTimeout(() => console.log("Timer expired"), 0);
process.nextTick(() => console.log("process.nextTick"));

function printA() {
  console.log("a=", a);
}
printA();

console.log("Last line of the file.");

Output:

a= 100
Last line of the file.
process.nextTick
Promise
Timer expired
setImmediate
File Reading CB

πŸ“„ Code Sample 3

setImmediate(() => console.log("setImmediate"));
setTimeout(() => console.log("Timer expired"), 0);
Promise.resolve().then(() => console.log("Promise"));

fs.readFile("./file.txt", "utf8", () => {
  setTimeout(() => console.log("2nd timer"), 0);
  process.nextTick(() => console.log("2nd nextTick"));
  setImmediate(() => console.log("2nd setImmediate"));
  console.log("File Reading CB");
});

process.nextTick(() => console.log("nextTick"));
console.log("Last line of the file.");

Output:

Last line of the file.
nextTick
Promise
Timer expired
setImmediate
File Reading CB
2nd nextTick
2nd setImmediate
2nd timer

πŸ“„ Code Sample 4

setImmediate(() => console.log("setImmediate"));
setTimeout(() => console.log("Timer expired"), 0);
Promise.resolve().then(() => console.log("Promise"));

fs.readFile("./file.txt", "utf8", () => {
  console.log("File Reading CB");
});

process.nextTick(() => {
  process.nextTick(() => console.log("inner nextTick"));
  console.log("nextTick");
});

console.log("Last line of the file.");

Output:

Last line of the file.
nextTick
inner nextTick
Promise
Timer expired
setImmediate
File Reading CB
0
Subscribe to my newsletter

Read articles from Gagan BN directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Gagan BN
Gagan BN