The Event Loop, Visualized

Break the Event Loop ⭐

Turn the knobs — every change re-runs the visualization instantly. Discover why order depends on where work is queued, not the order you wrote it in.

Timer delay
Promise chain depth
Blocking loop
Nested timeouts
1console.log('script start');
2
3setTimeout(() => {
4 console.log('timeout fired');
5}, 0);
6
7Promise.resolve()
8 .then(() => { console.log('microtask 1'); })
9 .then(() => { console.log('microtask 2'); })
10 .then(() => { console.log('microtask 3'); })
11 .then(() => { console.log('microtask 4'); })
12 .then(() => { console.log('microtask 5'); });
13
14console.log('script end');

The script starts executing top to bottom on the call stack.

1 / 25

Call stack

script

Web APIs — browser land

The event loop — is the call stack empty? → drain the microtask queue first → then take one callback.

Microtask queue — priority

Callback queue

Console

Fig. — your settings, as the event loop runs them · step 1 of 25

The URL holds these settings — copy it to share this experiment.

What each knob does

Every example on this site is a fixed snippet. This page is the opposite: one program with four dials, so you can change a single variable and see exactly which part of the output moves. The code on the left is regenerated from your settings each time, and the figure re-runs it step by step.

Timer delay

The delay passed to setTimeout is a minimum, not a schedule. Whether you pick 0ms or 1000ms, the callback lands in the callback queue and still waits for the call stack to empty and for every pending microtask to finish. A longer delay only changes how long it sits in Web APIs first.

Promise chain depth

Each .then schedules the next one as a new microtask, and the event loop drains the whole microtask queue before it takes a single timer callback. Push the depth to 100 and the zero-delay timeout waits behind all hundred steps. That is the same mechanism behind microtask starvation.

Blocking loop

A synchronous loop holds the call stack. While it runs, nothing else can: no timers, no promise callbacks, no clicks, no rendering. Raising the iteration count does not reorder the output, it just delays everything queued behind it, which is exactly what a frozen page feels like.

Nested timeouts

A setTimeout scheduled from inside another timer callback goes to the back of the callback queue, so it runs on a later turn of the loop. Any microtasks queued in between still jump ahead of it.

Common questions

Why does setTimeout(fn, 0) run after a promise?

Promise callbacks are microtasks and timers are tasks. After the current script finishes, the event loop empties the microtask queue completely before it picks up the next task, so the promise always wins.

Can a long loop make a timer fire late?

Yes. The timer can expire on time, but its callback cannot run until the call stack is empty. A loop that takes 200ms delays a 10ms timer to at least 200ms.

Step through the fixed examples →·Run your own code →