DEV Community

Alex Chen
Alex Chen

Posted on

The JavaScript Event Loop Explained Simply

The JavaScript Event Loop Explained Simply

Understanding the event loop makes you a better developer.

The Big Picture

┌───────────────────────────────────┐
│                                   │
│   ┌───────┐                      │
│   │ Call  │  Your code runs here  │
│   │ Stack │  (one thing at a time)│
│   └───┬───┘                      │
│       │                           │
│       ▼                           │
│   ┌───────────┐                   │
│   │ Web APIs  │ ← setTimeout,    │
│   │           │   fetch, DOM     │
│   └─────┬─────┘                   │
│         │                         │
│         ▼                         │
│   ┌───────────┐                   │
│   │ Callback  │  Waiting to run   │
│   │ Queue     │                   │
│   └─────┬─────┘                   │
│         │                         │
│         ▼ (when stack is empty)   │
│   ┌───────────┐                   │
│   │ Event Loop│ ← Picks next      │
│   │           │   callback        │
│   └───────────┘                   │
│                                   │
└───────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

How It Works (Step by Step)

console.log('1');

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

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

console.log('4');

// Output: 1, 4, 3, 2
// NOT: 1, 2, 3, 4!
Enter fullscreen mode Exit fullscreen mode
Timeline:

1. console.log('1') → runs immediately → "1"
2. setTimeout(fn, 0) → sends callback to Web API → timer starts
3. Promise.resolve().then(fn) → puts microtask in microtask queue
4. console.log('4') → runs immediately → "4"
5. Call stack empty! Event loop checks:
   a. Microtask queue first → "3" (Promise callbacks are microtasks!)
6. Check macrotask queue → "2" (setTimeout is macrotask)
Enter fullscreen mode Exit fullscreen mode

Microtasks vs Macrotasks

// Microtasks (higher priority — always run first):
// - Promise.then/catch/finally
// - MutationObserver
// - queueMicrotask()

// Macrotasks (lower priority):
// - setTimeout/setInterval
// - setImmediate (Node.js)
// - I/O operations
// - UI rendering

// Example:
console.log('start');

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

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

queueMicrotask(() => console.log('microtask'));

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

// Output:
// start
// promise 1
// microtask          ← microtasks run before any macrotask
// promise 2          ← chained microtasks run together
// setTimeout          ← now macrotasks
// setTimeout 2
Enter fullscreen mode Exit fullscreen mode

Practical Implications

Why setTimeout(fn, 0) Isn't Instant

// Even with 0ms delay, it waits for:
// 1. Current synchronous code to finish
// 2. All pending microtasks to complete
// 3. Its turn in the macrotask queue

console.log('A');
setTimeout(() => console.log('B'), 0);
for (let i = 0; i < 100000000; i++) {} // Blocking operation!
console.log('C');

// Output: A, C, B (not A, B, C!)
// The for-loop blocks everything until it finishes
Enter fullscreen mode Exit fullscreen mode

Non-Blocking I/O (Why Node.js Is Fast)

// Node.js doesn't wait for I/O — it continues executing!

const fs = require('fs');

console.log('Reading file...');

fs.readFile('/path/to/file.txt', 'utf8', (err, data) => {
  // This callback runs LATER when file is ready
  console.log('File content:', data.length, 'bytes');
});

console.log('This logs BEFORE the file finishes reading!');
// Because readFile is non-blocking — Node moves on immediately
Enter fullscreen mode Exit fullscreen mode

Starving the Event Loop

// ❌ This blocks the entire server!
app.get('/heavy', (req, res) => {
  const result = computeHeavyThing(10000000); // Synchronous blocking!
  res.json({ result });
});
// While this runs, NO other request can be processed!

// ✅ Break up heavy work:
app.get('/heavy', async (req, res) => {
  const result = await computeInChunks(10000000); // Yields control periodically
  res.json({ result });
});

function computeInChunks(n) {
  return new Promise(resolve => {
    let result = 0;
    let i = 0;

    function chunk() {
      const end = Math.min(i + 100000, n);
      for (; i < end; i++) {
        result += Math.sqrt(i);
      }

      if (i < n) {
        // Yield to event loop, continue later
        setImmediate(chunk);
      } else {
        resolve(result);
      }
    }

    chunk();
  });
}
Enter fullscreen mode Exit fullscreen mode

requestAnimationFrame vs setTimeout for Animation

// ❌ setTimeout for animation (runs regardless of browser state)
setInterval(() => {
  updatePosition();
}, 16); // ~60fps but not synced with display refresh

// ✅ requestAnimationFrame (syncs with display, pauses when tab hidden)
function animate() {
  updatePosition();
  requestAnimationFrame(animate); // Schedule next frame
}
requestAnimationFrame(animate);

// Benefits:
// - Runs at optimal time (usually 60fps)
// - Pauses when tab is not visible (saves battery!)
// - Synced with GPU/compositor
Enter fullscreen mode Exit fullscreen mode

Debugging Async Code

// Use this pattern to trace execution order:
const log = (msg) => console.log(`${new Date().toISOString().slice(11,19)} ${msg}`);

log('Start');
setTimeout(() => log('Timeout'), 100);
Promise.resolve().then(() => log('Promise'));
log('End');

// Output with timestamps shows exact order:
// 12:34:56.789 Start
// 12:34:56.790 End
// 12:34:56.791 Promise  ← microtask, almost instant
// 12:34:57.790 Timeout  ← macrotask, after ~1000ms
Enter fullscreen mode Exit fullscreen mode

Quick Reference

Concept Description
Call Stack Where your code executes (LIFO)
Web APIs Browser/Node features (setTimeout, fetch, etc.)
Callback Queue Where completed async tasks wait
Event Loop Moves callbacks from queue to stack when empty
Microtask Queue Higher priority (Promises, MutationObserver)
Macrotask Queue Lower priority (setTimeout, setInterval)
Blocking Long sync code prevents anything else from running
Non-blocking Async code yields control while waiting

Did this help you understand the event loop? Any questions?

Follow @armorbreak for more JS fundamentals.

Top comments (0)