Advanced 3 min · March 06, 2026

Event Loop in JavaScript

How the JavaScript event loop works — call stack, Web APIs, callback queue, microtask queue, and why setTimeout(fn, 0) does not run immediately.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
Quick Answer
  • JavaScript is single-threaded but handles async operations through the event loop
  • Call stack executes synchronous code; async callbacks wait in queues
  • Event loop moves callbacks to stack only when stack is empty
  • Microtasks (Promises) run before macrotasks (setTimeout)
  • Performance trap: a heavy sync task blocks all async work until done
  • Production insight: UI freezes if event loop is blocked for >50ms

The Call Stack

The call stack is a LIFO (last in, first out) data structure that tracks which function is currently executing. When a function is called, it is pushed on. When it returns, it is popped off. If the stack is busy, nothing else can happen—this is why we say JavaScript is 'blocking' by nature.

Async Operations and the Queue

When you call setTimeout or fetch, JavaScript engine doesn't wait. It hands the work off to the environment's Web APIs (in browsers) or C++ APIs (in Node.js). Your code continues running immediately. When the timer expires or the data returns, the callback is placed in the Macrotask Queue (also known as the Task Queue). It sits there patiently until the Call Stack is completely clear.

Microtask Queue — Promises Run First

Not all queues are created equal. JavaScript prioritizes the Microtask Queue (used by Promises and MutationObserver). After the current synchronous task finishes, the Event Loop will drain the entire Microtask Queue before it even looks at the Macrotask Queue. If a microtask schedules another microtask, that new one also runs before the next macrotask (like a setTimeout).

Why This Matters — Blocking the Event Loop

Because the Event Loop can only move a task to the stack when the stack is empty, a heavy calculation (like finding a large prime number) will 'block' the loop. During this time, the browser cannot render updates, and the UI becomes unresponsive. This is why we offload heavy CPU tasks to Web Workers or break them into asynchronous chunks.

Node.js Event Loop Phases (libuv)

Node.js extends the event loop with additional phases via libuv. The order is: timers, pending I/O callbacks, idle/prepare, poll, check (setImmediate), close callbacks. process.nextTick() runs between each phase, before the microtask queue. This explains why setImmediate vs setTimeout ordering can be non-deterministic depending on I/O.

Microtask vs Macrotask Queue
AspectMicrotask QueueMacrotask Queue
PriorityHigher — drained after every macrotaskLower — only one per loop iteration
ExamplesPromise.then, queueMicrotask, MutationObserversetTimeout, setInterval, I/O, UI events
Scheduling recursionCan starve macrotasks if recursiveRecursion yields control after each task
Execution contextBetween macrotask and next renderAfter microtasks, before render

Key Takeaways

  • JavaScript is single-threaded — only one function executes at a time in the Call Stack.
  • The Event Loop is a continuous process that monitors the stack and the queues to decide what runs next.
  • The 'Handoff': Synchronous Code > Microtasks (Promises) > Macrotasks (setTimeout/IO).
  • setTimeout(fn, 0) does not mean 'run immediately' — it means 'queue this for the next available tick after the stack is clear'.
  • Blocking the main thread is a cardinal sin in JS; it freezes the entire environment (UI/Node.js server).
  • process.nextTick() in Node.js runs before microtasks and can starve I/O if used recursively — prefer setImmediate.

Common Mistakes to Avoid

  • Assuming setTimeout(fn, 0) runs immediately after current code
    Symptom: Code after setTimeout executes before the callback, leading to race conditions where DOM updates expected from the callback aren't ready.
    Fix: Use microtasks (Promise.resolve().then()) for immediate after current sync, or understand that setTimeout always waits for a full event loop cycle.
  • Blocking the Event Loop with synchronous loops in async functions
    Symptom: UI freezes during async operations. The async function is awaited, but inside it a while loop blocks for seconds.
    Fix: Move long synchronous work to a Web Worker (browser) or worker_threads (Node.js). Alternatively, chunk the work with requestAnimationFrame or setTimeout.
  • Creating microtask recursion without a termination condition
    Symptom: UI hangs, setTimeout callbacks never fire, browser tab becomes unresponsive. The microtask queue grows infinitely.
    Fix: Add a depth counter; after N iterations, switch to setTimeout to allow other macrotasks to process.

Interview Questions on This Topic

  • QPredict the output order of: console.log, setTimeout(0), Promise.resolve().then(), and process.nextTick() (in Node.js).SeniorReveal
    The order is: console.log → process.nextTick → Promise.then → setTimeout. Explanation: Synchronous code runs first. After sync, the event loop processes process.nextTick (special queue) before microtasks. Then the microtask queue (Promise) is drained. Finally the macrotask queue (setTimeout) is visited. So nextTick edges out Promise.then.
  • QWhy might a recursive function that uses setTimeout() not cause a 'Maximum call stack size exceeded' error, while a standard recursive function does?Mid-levelReveal
    Because setTimeout places the recursive call into the macrotask queue, which runs when the call stack is empty. Each invocation starts with a fresh stack frame; the previous frame has already been popped. In contrast, synchronous recursion pushes frames onto the stack without ever popping until it returns or hits the limit.
  • QExplain how the 'Starvation' of the macrotask queue can happen if a microtask keeps adding more microtasks to its queue.Mid-levelReveal
    When a microtask (e.g., Promise.then) schedules another microtask, the event loop continues draining the microtask queue until it is empty before checking macrotasks. If every microtask creates another, the macrotask queue never gets a chance to run (its callbacks are starved). This blocks UI rendering, I/O callbacks, and timers. Mitigation: add a recursion limit and switch to setTimeout after a threshold.
  • QIn the context of the Event Loop, why is it usually better to perform heavy calculations in a Web Worker rather than the main thread?JuniorReveal
    Because heavy calculations block the main thread's Event Loop, preventing it from processing UI updates, user interactions, and other callbacks. Web Workers run in a separate OS thread with their own Event Loop. Communication is via postMessage, which is asynchronous. This keeps the main thread responsive (able to render at 60fps) and avoids the 'Heavy page' warning.
  • QLeetCode Style: Implement a basic 'Task Scheduler' that prioritizes tasks based on whether they are marked as 'urgent' (Microtask) or 'standard' (Macrotask).SeniorReveal
    Use two queues internally: one microtask queue (processed immediately via Promise) and one macrotask queue (processed via setTimeout(0)). expose a method schedule(task, priority). For urgent, wrap in Promise.resolve().then(() => task). For standard, use setTimeout(task, 0). Ensure that urgent tasks always run before standard tasks by leveraging the event loop's natural priority.

Frequently Asked Questions

What is the difference between the microtask queue and the macrotask queue?

Microtasks include Promise callbacks (.then, .catch, .finally) and queueMicrotask. Macrotasks include setTimeout, setInterval, setImmediate (Node.js), and I/O callbacks. The Event Loop will always drain the entire microtask queue completely after every macrotask before moving to the next one in line.

If setTimeout(fn, 0) does not run immediately, when exactly does it run?

It runs after all currently executing synchronous code has finished and the microtask queue has been fully emptied. The '0ms' delay is essentially a request to run the function as soon as possible on the next 'tick' of the event loop.

How does async/await relate to the event loop?

async/await is built on top of Promises. When the engine hits an await keyword, the execution of that specific function is paused, and it yields control back to the main thread. The code following the await is treated like a callback in the microtask queue, which executes once the awaited promise resolves.

Does Node.js have a different event loop than the browser?

While the core concept is identical, Node.js uses the libuv library which has additional phases (Poll, Check, Close callbacks) and unique features like process.nextTick(), which has even higher priority than standard microtasks.

What is the 'Heavy page' warning in Chrome and how to avoid it?

Chrome shows a 'Heavy page' warning when the main thread is blocked for more than 1 second, indicating the Event Loop is frozen. To avoid it, keep synchronous work under 50ms, use Web Workers for heavy computation, or chunk work using requestIdleCallback or setTimeout.

🔥

That's Advanced JS. Mark it forged?

3 min read · try the examples if you haven't

Previous
async and await in JavaScript
4 / 27 · Advanced JS
Next
Prototypes and Inheritance in JS