The Event Loop, Visualized

Event loop interview questions

Every question here is answered by an animation you can step through β€” so if you get one wrong, you can watch exactly where your model diverged. Predict first; the reveal is one click away.

warm-up The model everything else is built on.

  1. What order do the logs appear in?

    One thing at a time

    1function makeCoffee() {
    2 console.log('coffee ready');
    3}
    4
    5console.log('open the shop');
    6makeCoffee();
    7console.log('close the shop');
    • Aopen the shop β†’ coffee ready β†’ close the shop
    • Bcoffee ready β†’ open the shop β†’ close the shop
    • Copen the shop β†’ close the shop β†’ coffee ready
    Reveal the answer β†’

    A. open the shop β†’ coffee ready β†’ close the shop

    Before any of the async machinery makes sense, the call stack has to. JavaScript runs on a single thread with a single stack: calling a function pushes a frame on top, returning pops it off, and only the topmost frame is ever running.

    Watch it run, step by step β†’
  2. What does the 0 in setTimeout(fn, 0) actually mean?

    setTimeout(0), alone

    1console.log('before');
    2
    3setTimeout(() => {
    4 console.log('later');
    5}, 0);
    6
    7console.log('after');
    • ARun in exactly 0 milliseconds, guaranteed
    • BRun before any other console.log
    • CRun immediately, interrupting the current line
    • DRun after the current code finishes β€” at the earliest
    Reveal the answer β†’

    D. Run after the current code finishes β€” at the earliest

    setTimeout(fn, 0) is the shortest possible demonstration that a delay is a minimum, not a schedule. The callback does not run in zero milliseconds, and it does not run "as soon as possible" in any intuitive sense β€” it runs when the call stack is empty and the loop reaches it.

    Watch it run, step by step β†’
  3. Promise.resolve() is already resolved. When does its .then callback run?

    A promise, alone

    1console.log('start');
    2
    3Promise.resolve().then(() => {
    4 console.log('from the promise');
    5});
    6
    7console.log('end');
    • AAfter a minimum 4ms delay, like a timer
    • BAfter all the synchronous code finishes
    • COnly if you await it
    • DImmediately, on the same line
    Reveal the answer β†’

    B. After all the synchronous code finishes

    A promise that is already resolved still does not call your .then callback right away. Promise.resolve() has nothing to wait for, and yet "from the promise" prints after "end". That surprises people, and the reason is worth being precise about.

    Watch it run, step by step β†’

core The ones that actually get asked.

  1. What order do the logs appear in?

    Promise vs setTimeout

    1console.log('start');
    2
    3setTimeout(() => {
    4 console.log('timeout');
    5}, 0);
    6
    7Promise.resolve().then(() => {
    8 console.log('promise');
    9});
    10
    11console.log('end');
    • Astart β†’ end β†’ timeout β†’ promise
    • Bstart β†’ timeout β†’ promise β†’ end
    • Cstart β†’ end β†’ promise β†’ timeout
    • Dstart β†’ promise β†’ end β†’ timeout
    Reveal the answer β†’

    C. start β†’ end β†’ promise β†’ timeout

    This is the interview question, and the answer is start, end, promise, timeout. Both callbacks are scheduled at essentially the same moment, and the timer even becomes ready first β€” yet the promise callback still wins.

    Watch it run, step by step β†’
  2. What order do the logs appear in?

    .then after .then

    1Promise.resolve()
    2 .then(() => {
    3 console.log('first');
    4 })
    5 .then(() => {
    6 console.log('second');
    7 });
    8
    9console.log('sync');
    • Afirst β†’ second β†’ sync
    • Bsync β†’ second β†’ first
    • Csync β†’ first β†’ second
    • Dfirst β†’ sync β†’ second
    Reveal the answer β†’

    C. sync β†’ first β†’ second

    A .then chain is not a single unit of work. Each link is its own microtask, scheduled only when the previous link has produced a value β€” which is why "sync" prints first, then "first", then "second", instead of the whole chain resolving in one go.

    Watch it run, step by step β†’
  3. What does this loop log?

    setTimeout inside a for loop

    1for (var i = 0; i < 3; i++) {
    2 setTimeout(() => {
    3 console.log(i);
    4 }, 0);
    5}
    6
    7console.log('loop finished, i =', i);
    • A0, 1, 2
    • B3, 3, 3
    • C1, 2, 3
    • D0, 0, 0
    Reveal the answer β†’

    B. 3, 3, 3

    This is the most-asked JavaScript interview question that is really two questions wearing one coat: a closure question and an event loop question. The closure half is why every callback sees the same i. The event loop half is why every callback runs after the loop has already finished.

    Watch it run, step by step β†’
  4. What order do the logs appear in?

    async/await, unmasked

    1async function order() {
    2 console.log('ordering');
    3 await Promise.resolve();
    4 console.log('order arrived');
    5}
    6
    7order();
    8console.log('walk away');
    • Aordering β†’ walk away β†’ order arrived
    • Bordering β†’ order arrived β†’ walk away
    • Cwalk away β†’ ordering β†’ order arrived
    Reveal the answer β†’

    A. ordering β†’ walk away β†’ order arrived

    async and await are syntax over the same promise machinery, and this scene shows the moment that makes it obvious: the function leaves the call stack mid-body. "ordering" prints, the function hits await, and it stops β€” control returns to the caller, which is how "walk away" prints before "order arrived".

    Watch it run, step by step β†’
  5. Roughly how long until "done: a, b" prints?

    Promise.all β€” waiting in parallel

    1function wait(ms, label) {
    2 return new Promise((resolve) => {
    3 setTimeout(() => resolve(label), ms);
    4 });
    5}
    6
    7const a = wait(100, 'a');
    8const b = wait(100, 'b');
    9console.log('both timers already running');
    10
    11Promise.all([a, b]).then((results) => {
    12 console.log('done:', results.join(', '));
    13});
    • A~200ms β€” the waits run one after another
    • B~0ms β€” Promise.all does not wait
    • C~100ms, but the results may print as "b, a"
    • D~100ms β€” the two waits overlap
    Reveal the answer β†’

    D. ~100ms β€” the two waits overlap

    The mistake this scene fixes is believing that await starts the work. It does not. Calling wait(100, "a") starts the timer immediately, because the Promise executor runs synchronously. await only decides when you stop and collect the result.

    Watch it run, step by step β†’
  6. When the response arrives, where does the .then callback go?

    fetch β€” a real network wait

    1console.log('requesting');
    2
    3fetch('/api/user')
    4 .then((res) => res.json())
    5 .then((user) => {
    6 console.log('got', user.name);
    7 });
    8
    9console.log('not waiting');
    • AThe task queue, behind any waiting setTimeout callbacks
    • BIt stays in Web APIs until the next repaint
    • CThe microtask queue β€” it runs before any waiting timer callback
    • DStraight onto the call stack, interrupting whatever is running
    Reveal the answer β†’

    C. The microtask queue β€” it runs before any waiting timer callback

    fetch is the clearest demonstration of why the event loop exists. The network takes tens or hundreds of milliseconds, and the main thread spends none of it waiting. The request is handed to the browser, the stack empties, and the page stays responsive to clicks and animation the whole time.

    Watch it run, step by step β†’
  7. Two fast clicks arrive while the stack is busy. What happens?

    Clicks are just tasks

    1button.addEventListener('click', () => {
    2 console.log('clicked');
    3});
    4
    5console.log('listening');
    6
    7// the user now clicks twice, fast
    • ABoth handlers queue as tasks and run one per loop turn, in order
    • BBoth handlers run as microtasks, before any pending timer
    • CThe handlers interrupt the running code immediately
    • DThe second click is dropped because the first has not run yet
    Reveal the answer β†’

    A. Both handlers queue as tasks and run one per loop turn, in order

    Event listeners feel special β€” they seem to fire "when the user does something" β€” but the event loop treats them like everything else. The browser detects the click, then queues your listener as a task. It waits in the same queue as setTimeout callbacks, behind whatever is already there.

    Watch it run, step by step β†’

advanced Where mental models usually break.

  1. What is the output order?

    The checkpoint runs between every task

    1setTimeout(() => {
    2 console.log('task 1');
    3 Promise.resolve().then(() => console.log('micro 1'));
    4}, 0);
    5
    6setTimeout(() => {
    7 console.log('task 2');
    8 Promise.resolve().then(() => console.log('micro 2'));
    9}, 0);
    • Amicro 1 β†’ micro 2 β†’ task 1 β†’ task 2
    • Btask 1 β†’ micro 1 β†’ micro 2 β†’ task 2
    • Ctask 1 β†’ micro 1 β†’ task 2 β†’ micro 2
    • Dtask 1 β†’ task 2 β†’ micro 1 β†’ micro 2
    Reveal the answer β†’

    C. task 1 β†’ micro 1 β†’ task 2 β†’ micro 2

    Most people learn "microtasks run before tasks" from the classic promise-vs-setTimeout example and quietly generalise it into "all microtasks run, then all tasks". This scene breaks that. The microtask queue is drained after every single task, not once at the end of the script.

    Watch it run, step by step β†’
  2. When does "my table?!" print?

    Starving the loop

    1function feedVIPs() {
    2 queueMicrotask(feedVIPs);
    3}
    4
    5setTimeout(() => {
    6 console.log('my table?!');
    7}, 0);
    8
    9queueMicrotask(feedVIPs);
    • ANever β€” the microtask queue never empties
    • BAfter the first feedVIPs finishes
    • CAfter roughly 4ms, like any clamped timer
    • DImmediately β€” tasks have priority over microtasks
    Reveal the answer β†’

    A. Never β€” the microtask queue never empties

    Microtask priority is usually described as a feature. This scene shows the failure mode on the other side of it: a microtask that queues another microtask, forever. The checkpoint is defined to drain until the queue is empty, and this queue is never empty, so the checkpoint never ends.

    Watch it run, step by step β†’
  3. What is the output order?

    requestAnimationFrame vs setTimeout

    1console.log('start');
    2
    3setTimeout(() => {
    4 console.log('timeout');
    5}, 0);
    6
    7requestAnimationFrame(() => {
    8 console.log('rAF β€” right before paint');
    9});
    10
    11Promise.resolve().then(() => console.log('promise'));
    • Astart β†’ promise β†’ rAF β†’ timeout
    • Bstart β†’ timeout β†’ promise β†’ rAF
    • Cstart β†’ rAF β†’ promise β†’ timeout
    • Dstart β†’ promise β†’ timeout β†’ rAF
    Reveal the answer β†’

    D. start β†’ promise β†’ timeout β†’ rAF

    requestAnimationFrame is not a queue you have met so far. It is neither a task nor a microtask: the callback goes on the document's animation frame list, and the browser services that list during its render step β€” after the current task and its microtask checkpoint, immediately before style, layout and paint.

    Watch it run, step by step β†’
  4. The timer expired after 0ms. When does "am I late?" print?

    Blocking the loop

    1setTimeout(() => {
    2 console.log('am I late?');
    3}, 0);
    4
    5console.log('start spinning');
    6
    7while (true) {
    8 // spinning forever…
    9}
    • ANever β€” the while loop never lets the event loop take a turn
    • BRight after "start spinning"
    • CWhen the browser force-kills the loop after ~5 seconds
    • DAbout 0ms after the loop starts, interrupting it
    Reveal the answer β†’

    A. Never β€” the while loop never lets the event loop take a turn

    Every other example on this site is about ordering. This one is about the thing ordering depends on: the loop only gets a turn when the call stack is empty, and a synchronous loop never lets that happen.

    Watch it run, step by step β†’
  5. In Node, what is the output order?

    Node: nextTick, promises, timers, immediate

    1setTimeout(() => console.log('timeout'), 0);
    2setImmediate(() => console.log('immediate'));
    3
    4process.nextTick(() => console.log('nextTick'));
    5Promise.resolve().then(() => console.log('promise'));
    6
    7console.log('sync');
    • Async β†’ timeout β†’ immediate β†’ nextTick β†’ promise
    • Bsync β†’ nextTick β†’ promise β†’ immediate β†’ timeout
    • Csync β†’ promise β†’ nextTick β†’ timeout β†’ immediate
    • Dsync β†’ nextTick β†’ promise β†’ timeout β†’ immediate
    Reveal the answer β†’

    D. sync β†’ nextTick β†’ promise β†’ timeout β†’ immediate

    Node runs the same JavaScript semantics as the browser but a different loop underneath. Instead of "run one task, drain microtasks", libuv walks a fixed round of phases β€” timers, pending callbacks, idle/prepare, poll, check, close callbacks β€” and each phase has its own queue of callbacks to run.

    Watch it run, step by step β†’

Everything else people ask

Each one is answered on the page that animates it.

Got a snippet of your own you cannot explain? Paste it into the playground β†’