Interactive Guide

The JavaScript Event Loop, Visualized in 3D

An interactive 3D playground: step through the call stack, Web APIs, and task queues one event at a time.

Why does a 0ms timer lose to a promise? Step through an interactive 3D simulation of the JavaScript event loop — call stack, Web APIs, microtasks and macrotasks — and watch the answer happen.

AN
Arfin Nasir
Jun 11, 2026
7 min read
0 sections
The JavaScript Event Loop, Visualized in 3D
#javascript#event-loop#async#simulation
Interactive Deep Dive

The JavaScript Event Loop, Visualized

Why does a 0ms timer lose to a promise? Stop memorizing the answer — watch it happen on a 3D runtime stage you can orbit, step through, and replay.


Every JavaScript interview eventually arrives at the same question: "What does this print?" — followed by a snippet mixing console.log, setTimeout(..., 0), and a resolved promise. Most developers can recite the answer. Far fewer can explain why, because the explanation lives in a part of the runtime you never see: the event loop.

The event loop isn't part of the JavaScript language. You won't find it in the ECMAScript spec's core algorithms. It's the coordination layer the host environment (the browser, Node.js) wraps around the engine — the thing that decides, every time the call stack empties, what runs next.

JavaScript is single-threaded. The event loop is how a single thread fakes doing a hundred things at once — without ever doing two at the same time.

— The mental model this article builds

Instead of another box-and-arrow diagram, this article ships with a fully interactive 3D simulation. Every concept below is something you can watch happen — one event at a time.

▶ Interactive: The Event Loop, Live

This is not a video. Drag to orbit the stage, scroll to zoom, and press Step to advance one runtime event at a time — or Play to watch the whole run. Use the button for fullscreen, switch scenarios from the dropdown, or open the full-page version.


1. The Four Stations of the Runtime

The simulation stage has four platforms, and they map one-to-one onto how the browser actually executes your code. Spin the camera around them as you read.

The Stage, Decoded

  • Call Stack (blue) — the only place code actually runs. LIFO. One frame at a time, one thread, no exceptions.
  • Web APIs (teal) — the browser's own threads. Timers, fetch, DOM events live here while they wait. This is where the concurrency really happens.
  • Microtask queue (lavender) — promise callbacks, queueMicrotask, MutationObserver. FIFO, but with absolute priority.
  • Macrotask queue (amber) — timer callbacks, I/O, UI events. FIFO, processed one per loop turn.

The glowing ring in the center is the loop itself. Its entire job description fits in one sentence: when the stack is empty, drain every microtask, then run exactly one macrotask, then repeat. Everything surprising about async JavaScript falls out of that sentence.


2. Scenario 1 — Promise vs setTimeout

The default scenario in the simulation runs this classic:

console.log('Start');

setTimeout(() => {
  console.log('Timeout');
}, 0);

Promise.resolve().then(() => {
  console.log('Promise');
});

console.log('End');

Step through it and watch four things that diagrams never make visceral:

  • setTimeout never waits on the stack. The call itself takes microseconds — it hands the timer to a Web API thread (watch the block fly down the teal lane) and pops immediately.
  • The timer "fires" while sync code is still running. The 0ms expires almost instantly, but its callback can only get as far as the macrotask queue. It cannot touch the stack while main() is on it.
  • The resolved promise skips the Web APIs entirely. Its .then callback goes straight to the microtask queue along the lavender lane.
  • The loop wakes only when main() pops. Then: microtasks first — 'Promise' prints before 'Timeout' even though setTimeout appeared first in the source.
The output: Start → End → Promise → Timeout. Sync code, then all microtasks, then one macrotask. Burn that ordering in — it explains 90% of async puzzles.

3. Scenario 2 — The Microtask Chain (and Starvation)

Switch the dropdown to "Microtask chain":

setTimeout(() => console.log('A'), 0);

Promise.resolve()
  .then(() => console.log('B'))
  .then(() => console.log('C'));

console.log('D');

The crucial moment happens at step 8. When the 'B' callback finishes, it resolves the next promise in the chain — which means 'C' joins the microtask queue mid-drain. And the loop's rule is merciless: the microtask queue must be completely empty before any macrotask gets a turn.

So the chain runs back-to-back — B, then C — while the poor timer callback waits. Output: D → B → C → A. An entire promise chain outranks a 0ms timer.

The Production Gotcha: Microtask Starvation

Because microtasks can enqueue more microtasks, a recursive promise chain can starve the macrotask queue forever — no timers fire, no clicks process, no rendering happens. If you've ever frozen a tab with a recursive Promise.resolve().then(loop), this is why. Recursive setTimeout is safe for polling; recursive microtasks are not.
*The render pipeline only gets a chance between macrotasks — another reason to keep microtask work short.


4. The Rules That Matter in Production

Event Loop Survival Checklist

  • DO use microtasks (queueMicrotask, resolved promises) when you need to run "right after this code, before anything else."
  • DO use setTimeout(fn, 0) to yield — let the browser render and process input between heavy chunks.
  • DO remember await is sugar for .then: everything after an await is a microtask.
  • DON'T block the stack. A 200ms synchronous loop means 200ms of frozen UI — no queue can interrupt running code.
  • DON'T chain microtasks recursively. That's the starvation pattern from Scenario 2.
  • DON'T trust setTimeout(fn, 0) to mean "now." It means "after the current task, all microtasks, and any earlier macrotasks."

Where Node.js Differs

Node's event loop (libuv) splits macrotasks into phases — timers, pending callbacks, poll, check (setImmediate), close. The microtask rule survives intact though: promise callbacks and process.nextTick drain between every phase transition, and nextTick outranks even promises. The mental model from the simulation transfers — Node just has more macrotask lanes.


Frequently Asked Questions

Is JavaScript really single-threaded?

The engine executes your code on one thread, yes. But the platform around it is heavily multi-threaded — timers, fetch, and DOM parsing run on browser threads (the teal station in the simulation). Web Workers give you real extra threads, each with its own event loop and no shared memory.

Where does async/await fit into this?

await suspends the function and registers the rest of it as a microtask, exactly like a .then callback. An async function runs synchronously until its first await — a detail that surprises people: the code before the first await is not deferred at all.

Why does the browser sometimes batch my DOM updates?

Rendering is not a queue citizen — it's a separate pipeline step the loop fits in between macrotasks, roughly 60 times a second. Multiple DOM writes inside one task get painted together. If you need to run code after layout but before paint, that's what requestAnimationFrame is for.

Is setTimeout(fn, 0) really 0 milliseconds?

No. Browsers clamp nested timers to ~4ms, background tabs to 1000ms+, and the callback still has to wait for the stack to clear and microtasks to drain. 0 means "as soon as the loop allows," not "immediately."


Final Thoughts

The event loop stops being confusing the moment you stop thinking of it as trivia and start seeing it as architecture: one stack that runs everything, browser threads that wait on everything, and two queues with brutally simple priority rules. Step through the simulation until the rules feel obvious — then go read your own codebase's async paths with new eyes.

I build interactive explanations like this one — and the production systems they describe. If your team is wrestling with async-heavy JavaScript, performance bottlenecks, or you just want engineering content that people actually understand, let's talk.


Want to work on something like this?

I help companies build scalable, high-performance products using modern architecture.