looptrace·explained

JavaScript Event Loop Explained — the Call Stack, Microtask & Macrotask Queues, Animated

Async ordering is not magic; it is one small loop with strict rules. This page animates those rules on real snippets — and every console order you will see here is proven by running the code in real Node, not asserted from memory. It is the guided tour behind looptrace, the step-through visualizer.

100% offline · no tracking Orders verified in Node v25.4.0 CSS-only animation
One turn of the loop: run a task → drain every microtask → take the next macrotask.

01 · The trap

Four lines. Most developers guess the order wrong.

This is the most evergreen JavaScript interview question there is — and a daily debugging trap. The naive guess is A B C D. Node prints something else.

console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));
console.log('D');
console — real Node output
  1. A
  2. D
  3. C
  4. B

✓ Order verified by executing this exact code in Node v25.4.0 on 2026-07-23 — re-proven by this site's test suite on every build.

Why? setTimeout queues a macrotask; .then queues a microtask — and microtasks always run first. The next three scenes show exactly why.

02 · One turn of the loop

Watch the queues do their jobs

The same four lines, played across the loop's lanes — exactly how looptrace's step player draws it. Crimson always marks the thing executing right now.

  1. 1 run script
  2. 2 drain microtasks
  3. 3 next macrotask

Call stack

setTimeout callback
.then callback
script

empty — turn complete

Microtask queue

.then → log('C')

drained

Macrotask queue

setTimeout → log('B')

drained

Console

  1. A
  2. D
  3. C
  4. B

1 · run script: script runs on the stack — logs A, queues the timeout (macrotask), queues the .then (microtask), logs D, then pops.

2 · microtask checkpoint: before anything else, the microtask queue is drained — the .then callback runs and logs C.

3 · next macrotask: only now is the timeout callback taken — it runs and logs B. The tick dot is back where it started.

03 · The drain rule

Microtasks drain fully — even ones queued mid-drain

Two timeouts are queued first, yet both promise callbacks still beat them — because a microtask queued during the checkpoint is drained in the same checkpoint.

setTimeout(() => console.log('t1'), 0);
setTimeout(() => console.log('t2'), 0);
Promise.resolve().then(() => {
  console.log('p1');
  Promise.resolve().then(() => console.log('p2'));
});
microtasks p1 p2 (queued mid-drain)
macrotasks t1 t2
  1. p1
  2. p2
  3. t1
  4. t2

Rule: the checkpoint empties the whole microtask queue before the loop may take one — just one — macrotask.

04 · await, demystified

await suspends the function — it never blocks the thread

At await, the function pauses and its continuation is queued as one microtask. Everything after the call keeps running first.

async function f() {
  console.log('1');
  await null;
  console.log('2');
}
f();
console.log('3');
  1. f() is called and runs synchronously — logs 1
  2. await null suspends f; its continuation is queued as one microtask
  3. the script continues past the call — logs 3, then pops
  4. microtask checkpoint: the continuation resumes f — logs 2
  1. 1
  2. 3
  3. 2

Modern semantics: since ES2019, an await on a resolved value costs exactly one microtask tick. looptrace models these modern semantics and says so.

05 · Proven, not asserted

Real Node is the referee for every order shown

Most event-loop diagrams show the order the author believes is right. looptrace — and this page — are different by construction.

All 36 looptrace snippets pass this gate, verified against Node v25.4.0 on 2026-07-23 — and this explainer runs the same proof on its own three demos in test/facts.test.js, so the page can never drift from reality.

06 · Enforced privacy

The browser itself blocks any send — connect-src 'none'

Privacy here is not a promise in a policy page; it is a Content-Security-Policy rule the browser enforces. This page — and looptrace — cannot make a network request.

Blocked — by the browser, every time.
  • No network path exists: fetch, XHR and WebSockets are all refused by the CSP.
  • No third parties: no CDN scripts, no external fonts, no analytics, no cookies.
  • Local only: looptrace stores your predict-mode streak — and this page your theme — in localStorage. Nothing ever leaves your device.
  • Check it yourself: open DevTools → Network. It stays empty after load.

07 · Inside looptrace

What you get in the visualizer

36 verified snippets

Grouped by topic — stack basics, setTimeout, .then chains, queueMicrotask, async/await, interview classics — difficulty-tagged and hash-linkable.

Step player, four live panels

Prev / next / autoplay / arrow keys over call stack, microtask queue, macrotask queue and console — with a plain-English caption for every single step.

Racetrack phase dial

A crimson tick dot advances around the loop — run script → drain microtasks → next macrotask — so you always know which phase you are in.

Predict-first mode

Arrange the expected console lines before playback, then reveal. Your correct-streak lives only in localStorage. The interview-prep hook.

Citations on every snippet

Each snippet names the WHATWG / MDN / V8 rule it demonstrates, plus the exact Node version and date its console order was verified against.

Export & honest limits

Copy any annotated step trace as Markdown or print it. Node-only phases (process.nextTick, setImmediate) are explained in prose — never simulated.

Now step through it yourself

Reading about the loop is one thing. Stepping a real snippet through the stack and queues — and predicting the order before you press play — is how it sticks.

Open looptrace — 36 proven snippets, free & offline

No sign-up. No tracking. Works offline once loaded.

08 · FAQ

Common questions

What is the difference between a microtask and a macrotask?

Macrotasks (like setTimeout callbacks) are taken one per event-loop turn. Microtasks (promise .then callbacks, queueMicrotask, await continuations) live in a separate queue that is drained completely after every task — even microtasks queued during the drain run before the next macrotask. That is why a promise callback always beats a setTimeout(0) callback.

Why does setTimeout(fn, 0) run after Promise.then?

setTimeout(fn, 0) only queues a macrotask; it never runs immediately. Before the event loop takes the next macrotask it performs a microtask checkpoint that drains the entire microtask queue — so every pending .then callback runs first, even if the timeout was queued earlier.

Are the console orders in these animations real or illustrative?

Real. Every snippet on this page is executed verbatim in a real Node child process (Node v25.4.0, verified 2026-07-23) by the site's own test suite, and the captured output must exactly match the order shown. looptrace applies the same rule to all 36 of its snippets: any snippet its model cannot predict is removed, never patched by hand.

Does this page work offline and is it private?

Yes. It is a static page with a strict Content-Security-Policy (connect-src 'none'), so the browser itself blocks any network request. No accounts, no analytics, no tracking; the only stored value is your theme choice, in your browser's localStorage.