Module F-12·14 min read

How the Node.js Event Loop is built from queues, and how the call stack you just learned governs every stack overflow you have seen.

JJS
Written by Jatin Jain Saraf · Senior Software Engineer

Module F-12 — Capstone: DSA in Production — The Node.js Event Loop, Call Stack, and Queues

What this module covers: Foundation closes by pointing two structures you've already built — the stack (Module F-6) and the queue (Module F-7) — directly at the runtime you've been writing every code snippet in this course inside. The Node.js event loop isn't a separate, exotic concept to memorize; it's your call stack and a handful of FIFO queues, working together under one simple rule.


One Thread, One Call Stack

Every piece of JavaScript you write executes on a single call stack (Module F-6) — one thread, one stack, no exceptions. When you call a function, a frame is pushed; when it returns, the frame is popped. This is true whether you're running a for loop, calling console.log, or handling an HTTP request.

typescript

There is no parallelism happening here — nothing else in your Node.js process runs while this code is executing, because the one call stack is busy. This single fact is the seed of everything else in this module.


What Happens When You Call setTimeout

Asynchronous APIs — setTimeout, file reads, network requests — don't run on the JavaScript call stack at all. They're handed off to libuv, the C library underneath Node.js that manages timers, file system operations, and networking outside of JavaScript's single thread. When the operation completes, libuv doesn't run your callback immediately — it places it into a queue (Module F-7), to be picked up later.

typescript

B prints last, not because the timer takes meaningful time, but because setTimeout's callback is only ever placed into a queue — it cannot run until the currently executing code finishes and the call stack is completely empty. This is the event loop's core rule: check the queues only when the call stack is empty, never interrupt code that's currently running. JavaScript has no equivalent of a thread being preempted mid-function by another callback — a running function always finishes (runs to completion) before anything queued gets a turn.


Two Queues, Not One

Node.js doesn't have a single callback queue — it has (at minimum) two distinct ones, checked in a specific order, and the difference explains a huge amount of async ordering confusion:

  • The microtask queue — resolved/rejected Promise callbacks (.then(), async/await continuations), and queueMicrotask().
  • The macrotask queuesetTimeout/setInterval callbacks, I/O completion callbacks (file reads, network responses), and setImmediate.

The rule: after any piece of code finishes running and the call stack empties, the entire microtask queue is drained completely — every microtask currently in it, including any new ones added while draining — before a single macrotask is allowed to run.

typescript

A and D run first — synchronous code occupying the call stack. Once the stack empties, the event loop checks the microtask queue before the macrotask queue, every single time — so the Promise.then() callback (C) runs before the setTimeout callback (B), even though the setTimeout was scheduled first in the source code. This is a direct, practical consequence of both queues being FIFO (Module F-7) but checked in a fixed priority order — microtasks always drain first, completely, before the next macrotask is considered.

Sign in to keep reading

The rest of this module is free — sign in with Google to unlock it and track your progress.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.