The Event Loop

How JavaScript handles async. Single-threaded but never blocked.

Single Thread

JavaScript runs one thing at a time — always.

// JavaScript has ONE thread of execution.
// It can only do one thing at a time.

console.log("First");   // runs
console.log("Second");  // waits for First to finish
console.log("Third");   // waits for Second to finish

// So how does async work?
// The EVENT LOOP coordinates between:
// 1. The Call Stack (where code runs)
// 2. Web APIs (where async operations wait)
// 3. Task Queues (where callbacks line up)

// JS itself is single-threaded,
// but the BROWSER is not — it has many threads
// for timers, network, DOM, etc.

The actors:

Call Stack — where functions execute (one at a time)

Web APIs — browser handles timers, fetch, DOM events

Task Queue — callbacks waiting for the stack to empty

Event Loop — moves tasks from queue → stack when empty

Call Stack

The stack tracks what's currently executing.

function multiply(a, b) { return a * b; }
function square(n) { return multiply(n, n); }
function printSquare(n) {
  const result = square(n);
  console.log(result);
}

printSquare(4);

// Call Stack progression:
// 1. [printSquare]
// 2. [printSquare, square]
// 3. [printSquare, square, multiply]
// 4. [printSquare, square]  ← multiply returns
// 5. [printSquare]          ← square returns
// 6. [printSquare, console.log]
// 7. [printSquare]          ← console.log returns
// 8. []                     ← printSquare returns

Functions push onto the stack when called, pop off when they return. Only the top frame is executing.

1 / 2

The Loop

The event loop's job: move queued tasks to the stack.

// The Event Loop algorithm (simplified):
// 
// while (true) {
//   1. Run all synchronous code until the stack is empty
//   2. Run ALL microtasks (promises, queueMicrotask)
//   3. Render update (if needed — ~60fps = every 16ms)
//   4. Pick ONE macrotask from the queue (setTimeout, etc.)
//   5. Go back to step 1
// }

// Visual:
//
//   ┌─────────────────────────────┐
//   │        Call Stack            │ ← code runs here
//   └─────────────┬───────────────┘
//                 │ empty?
//   ┌─────────────▼───────────────┐
//   │     Microtask Queue          │ ← drain ALL
//   └─────────────┬───────────────┘
//                 │ empty?
//   ┌─────────────▼───────────────┐
//   │      Render (if needed)      │
//   └─────────────┬───────────────┘
//                 │
//   ┌─────────────▼───────────────┐
//   │    Macrotask Queue           │ ← pick ONE
//   └─────────────┬───────────────┘
//                 │
//            loop back ↑

Task Queue

Where async callbacks wait their turn.

console.log("1: start");

setTimeout(() => {
  console.log("2: timeout");  // → macrotask queue
}, 0);

console.log("3: end");

// Output: "1: start", "3: end", "2: timeout"

// Why? Even with 0ms delay:
// 1. console.log("1: start") → runs immediately (stack)
// 2. setTimeout callback → goes to Web API, then queue
// 3. console.log("3: end") → runs immediately (stack)
// 4. Stack is now empty → event loop picks up timeout callback
// 5. console.log("2: timeout") → finally runs

// The key insight:
// setTimeout(fn, 0) doesn't mean "run immediately"
// It means "run as soon as the stack is empty"
// (minimum delay is actually ~4ms in browsers)
// Multiple tasks queue in order:
setTimeout(() => console.log("A"), 0);
setTimeout(() => console.log("B"), 0);
setTimeout(() => console.log("C"), 0);
// Output: A, B, C (FIFO — first in, first out)

// Longer delays queue later:
setTimeout(() => console.log("slow"), 100);
setTimeout(() => console.log("fast"), 10);
// Output: fast, slow (10ms fires before 100ms)

// But remember: times are MINIMUM delays
// If the stack is busy, the callback waits longer:
setTimeout(() => console.log("delayed"), 10);
// If synchronous code takes 500ms,
// this callback runs after ~500ms, not 10ms

Execution Order

Predict the order of async operations.

console.log("1");

setTimeout(() => console.log("2"), 0);

Promise.resolve().then(() => console.log("3"));

console.log("4");

// Output: 1, 4, 3, 2
//
// Why this order:
// "1" — synchronous, runs immediately
// setTimeout → callback goes to MACROTASK queue
// Promise.then → callback goes to MICROTASK queue
// "4" — synchronous, runs immediately
// Stack empty → drain microtasks first → "3"
// Then pick one macrotask → "2"

// Rule: Microtasks ALWAYS run before macrotasks
// (when both are waiting)
// More complex example:
console.log("A");

setTimeout(() => {
  console.log("B");
  Promise.resolve().then(() => console.log("C"));
}, 0);

Promise.resolve().then(() => {
  console.log("D");
  setTimeout(() => console.log("E"), 0);
});

console.log("F");

// Output: A, F, D, B, C, E
// A — sync
// F — sync
// D — microtask (runs before any macrotask)
// B — first macrotask (setTimeout from top)
// C — microtask created inside B's macrotask
// E — second macrotask (setTimeout from D's microtask)

FAQ

Common questions about the event loop.