February 15, 20268 min read

JavaScript Event Loop Explained with Interactive Examples

JavaScriptEvent LoopAsyncBrowser Runtime

Why this matters

JavaScript runs on a single call stack, but async APIs make it feel concurrent. The event loop is the mechanism that decides what runs next.

Rule to remember: when the call stack becomes empty, JavaScript drains the microtask queue first, then runs one task from the task queue, then repeats.

How JavaScript works (quick mental model)

JavaScript runs in a top-down, sequential way because it is single-threaded in the main runtime. That means statements are executed one by one on the call stack.

When async code is involved, the common execution pattern looks like:

  1. Synchronous code
  2. Promise callbacks (microtasks)
  3. Timeout and interval callbacks (tasks/macrotasks)

The three building blocks

  • Call Stack: current execution context. Synchronous code runs here.
  • Task Queue (Macrotask Queue): a FIFO queue for setTimeout, setInterval, DOM events, and I/O callbacks.
  • Microtask Queue: a FIFO queue for Promise.then/catch/finally, queueMicrotask, and MutationObserver.

The call stack itself behaves like LIFO (Last In, First Out), while both queues behave like FIFO (First In, First Out).

Example 1: Simple ordering

The code mixes sync logs, a Promise callback, and a setTimeout(0) callback.

Interactive Event Loop Demo

Simple Example

Synchronous logs vs Promise microtask vs setTimeout task

Code

console.log('A');

setTimeout(() => {
  console.log('B - timeout');
}, 0);

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

console.log('D');

Output Challenge

Test Yourself

Write the console output in order, one line per entry.

Step 1 / 8: Script starts on call stack

Global script enters the call stack.

Call Stack (Top First)

global()

Microtask Queue (Front First)

No microtasks waiting

Task Queue (Front First)

No tasks waiting

Console Output So Far

No console output yet.

Example 2: Slightly complex ordering

This version nests a microtask inside a timeout and a timeout inside a microtask.

Interactive Event Loop Demo

Complex Example

Nested microtasks and tasks with ordering across turns

Code

console.log('1');

setTimeout(() => {
  console.log('2 - timeout 1');

  Promise.resolve().then(() => {
    console.log('3 - microtask inside timeout');
  });
}, 0);

Promise.resolve().then(() => {
  console.log('4 - microtask 1');

  setTimeout(() => {
    console.log('5 - timeout inside microtask');
  }, 0);
});

console.log('6');

Output Challenge

Test Yourself

Write the console output in order, one line per entry.

Step 1 / 10: Script enters stack

Global script begins.

Call Stack (Top First)

global()

Microtask Queue (Front First)

No microtasks waiting

Task Queue (Front First)

No tasks waiting

Console Output So Far

No console output yet.

Example 3: Timeout is not zero

This shows that setTimeout(fn, 1000) means at least 1000ms, not exactly 1000ms.

Interactive Event Loop Demo

Timeout Not Zero Example

Why setTimeout with 1000ms is a minimum delay, not exact timing

Code

const start = Date.now();

setTimeout(() => {
  console.log('T1 ~1000ms', Date.now() - start);
}, 1000);

setTimeout(() => {
  console.log('T2 ~0ms', Date.now() - start);
}, 0);

Promise.resolve().then(() => {
  console.log('Microtask', Date.now() - start);
});

console.log('Sync end', Date.now() - start);

Output Challenge

Test Yourself

Write the console output in order, one line per entry.

Step 1 / 8: Script starts

Global script enters stack and sets reference start time.

Call Stack (Top First)

global()

Microtask Queue (Front First)

No microtasks waiting

Task Queue (Front First)

No tasks waiting

Console Output So Far

No console output yet.

Final checklist

  • Synchronous code runs first on the call stack.
  • After stack is empty, all microtasks run before the next task.
  • setTimeout(..., 0) still waits for a future task turn.
  • Timeout delay values are minimum thresholds, not exact execution timestamps.