DEV Community

Alex Chen
Alex Chen

Posted on

The JavaScript Event Loop Explained Simply

The JavaScript Event Loop Explained Simply

Why does setTimeout(callback, 0) not run immediately? Let me explain.

The Core Concept

JavaScript is single-threaded.
It can only do ONE thing at a time.

But wait — how can it handle:
- Click events while fetching data?
- Animations while processing user input?
- Multiple timers at once?

Answer: THE EVENT LOOP
Enter fullscreen mode Exit fullscreen mode

How It Works (Visual)

┌───────────────────────────────────┐
│           Call Stack              │  ← Code runs here (LIFO)
│  ┌─────┐                          │
│  │ log │  ← Currently executing   │
│  └─────┘                          │
│  ┌─────┐                          │
│  │ main│  ← Waiting to run        │
│  └─────┘                          │
└──────────┬────────────────────────┘
           │
           ▼
┌───────────────────────────────────┐
│         Web APIs                  │  ← Browser handles async ops
│  setTimeout, fetch, DOM, ...     │
└──────────┬────────────────────────┘
           │ (when complete)
           ▼
┌───────────────────────────────────┐
│       Callback Queue              │  ← Waiting their turn
│  [click] [timer] [fetch resolve]  │
└──────────┬────────────────────────┘
           │ (when stack is empty)
           ▼
     Event Loop picks up the next callback
Enter fullscreen mode Exit fullscreen mode

Step-by-Step Example

console.log('1');

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

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

console.log('4');
Enter fullscreen mode Exit fullscreen mode
What gets logged? Let's trace:

1. console.log('1') → runs immediately → output: "1"
2. setTimeout(cb, 0) → timer registered in Web APIs
3. Promise.then(cb) → microtask queued
4. console.log('4') → runs immediately → output: "1, 4"

Call Stack is now EMPTY.

Event Loop checks:
  1. Microtask Queue: has Promise callback → runs it → "3"
  2. Microtask Queue: empty now
  3. Task (Callback) Queue: has setTimeout → runs it → "2"

Final output: 1, 4, 3, 2

NOT 1, 2, 3, 4!
NOT 1, 4, 2, 3!

The Promise (microtask) ran BEFORE setTimeout (macrotask)!
Enter fullscreen mode Exit fullscreen mode

Microtasks vs Macrotasks

Priority Order (when stack is empty):

1. All Microtasks (run ALL of them before anything else)
   - Promise.then/catch/finally
   - queueMicrotask()
   - MutationObserver

2. One Macrotask (just ONE)
   - setTimeout/setInterval
   - I/O callbacks
   - UI rendering
   - requestAnimationFrame

3. Render (if needed)
   - Style calculation
   - Layout
   - Paint

4. Back to step 1 (check microtasks again!)
Enter fullscreen mode Exit fullscreen mode
// Proof of microtask priority:
console.log('start');

setTimeout(() => console.log('timeout'), 0);

Promise.resolve()
  .then(() => console.log('promise 1'))
  .then(() => console.log('promise 2'));

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

console.log('end');

// Output: start, end, promise 1, promise 3, promise 2, timeout
//
// Explanation:
// 1. sync: start, end
// 2. microtasks: promise 1, promise 3, promise 2 (all of them!)
// 3. macrotask: timeout (only after all microtasks done)
Enter fullscreen mode Exit fullscreen mode

Real-World Impact

Why Your UI Freezes

// ❌ Blocking the call stack
function heavy() {
  const start = Date.now();
  while (Date.now() - start < 3000) {
    // CPU-bound work for 3 seconds
    // During this time: NO clicks, NO animations, NOTHING
  }
}

heavy(); // Page is frozen for 3 seconds!

// ✅ Break it into chunks using setTimeout(0)
function heavyNonBlocking(chunks = 10) {
  let i = 0;

  function processChunk() {
    const start = Date.now();
    while (Date.now() - start < 50 && i < chunks) { // Max 50ms per chunk
      processChunkData(i);
      i++;
    }

    if (i < chunks) {
      setTimeout(processChunk, 0); // Yield to event loop between chunks
    }
  }

  processChunk();
}
Enter fullscreen mode Exit fullscreen mode

Why requestAnimationFrame Exists

// ❌ setInterval for animation (runs regardless of render cycle)
setInterval(updatePosition, 16); // ~60fps but not synced to screen refresh

// ✅ requestAnimationFrame (syncs to browser's paint cycle)
function animate() {
  updatePosition();
  requestAnimationFrame(animate); // Called right before next paint
}
requestAnimationFrame(animate);

// Runs at optimal rate for the display
// Pauses when tab is backgrounded (saves battery!)
// Automatically adjusts for monitor refresh rate
Enter fullscreen mode Exit fullscreen mode

Why async/await Doesn't Block

async function loadData() {
  console.log('before await');      // Sync — runs on call stack

  const data = await fetch(url);    // Yields control here!
                                    // Fetch happens in Web APIs
                                    // Rest of function becomes a microtask

  console.log('after await');       // This is a microtask callback
  return data;
}

console.log('calling loadData');    // Sync
loadData();                         // Starts executing, hits await, returns
console.log('after calling');       // Sync — continues immediately

// Output:
// calling loadData
// before await
// after calling
// after await (after fetch completes)
Enter fullscreen mode Exit fullscreen mode

Common Interview Questions

Q1: What's the output?

for (var i = 0; i < 5; i++) {
  setTimeout(() => console.log(i), 100);
}
// Answer: 5, 5, 5, 5, 5 (var is function-scoped, loop finishes first)

// Fix 1: let (block-scoped)
for (let i = 0; i < 5; i++) {
  setTimeout(() => console.log(i), 100);
}
// Answer: 0, 1, 2, 3, 4

// Fix 2: IIFE (closure)
for (var i = 0; i < 5; i++) {
  ((j) => setTimeout(() => console.log(j), 100))(i);
}
// Answer: 0, 1, 2, 3, 4
Enter fullscreen mode Exit fullscreen mode

Q2: What's the output?

async function async1() {
  console.log('async1 start');
  await async2();
  console.log('async1 end');
}

async function async2() {
  console.log('async2');
}

console.log('script start');
setTimeout(() => console.log('setTimeout'), 0);
async1();
new Promise(resolve => {
  console.log('promise1');
  resolve();
}).then(() => console.log('promise2'));
console.log('script end');

// Answer:
// script start
// async1 start
// async2          (await yields, then sync part of async2 runs)
// promise1
// script end
// async1 end      (microtask from await)
// promise2        (microtask from .then())
// setTimeout      (macrotask)
Enter fullscreen mode Exit fullscreen mode

Quick Reference

Task Where It Runs
Synchronous code Call Stack
DOM events Task Queue (macrotask)
setTimeout/setInterval Task Queue (macrotask)
fetch/XHR callbacks Microtask Queue
Promise .then/.catch Microtask Queue
queueMicrotask() Microtask Queue
requestAnimationFrame Before paint (not exactly a queue)

Does the event loop finally make sense?

Follow @armorbreak for more JavaScript content.

Top comments (0)