The JavaScript Event Loop

Master the JavaScript runtime — interactive event loop simulator, async pattern comparison (callbacks vs Promises vs async/await), and common pitfall debugger with live output.

By Forhad30 min readIntermediateInteractive Demo
The JavaScript Event Loop

JavaScript Runtime Architecture

JavaScript is single-threaded — it has one call stack and processes one thing at a time. But it handles asynchronous operations through the event loop, which coordinates between the call stack, Web APIs, and two task queues.

JavaScript Runtime Architecture

Click each component to understand its role. JavaScript is single-threaded but the browser provides concurrency through Web APIs.

📚Call Stack

LIFO (last-in, first-out) structure. Each function call pushes a frame; returning pops it. JavaScript has ONE call stack — this is why it's single-threaded.

The Event Loop Algorithm

while (true) {
  // 1. Execute the call stack until empty
  // 2. Drain ALL microtasks
  // 3. Render/paint if needed
  // 4. Pick ONE macrotask → push to call stack
  // 5. Go to step 1
}

🔑 Golden Rule: After the call stack empties: drain ALL microtasks → pick ONE macrotask → repeat. This is why Promise.then() always runs before setTimeout(cb, 0) — Promises go to the microtask queue which has higher priority.

Event Loop Simulator

Step through real code execution and watch items move between the call stack, microtask queue, and macrotask queue. This is the best way to build an intuition for execution order.

Event Loop Simulator

Step through code execution and watch items move between the call stack, microtask queue, and macrotask queue.

Source Code
console.log("1: Start");

setTimeout(() => {
  console.log("4: setTimeout");
}, 0);

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

console.log("2: End");

Call Stack

console.log("1: Start")

Micro Queue

empty

Macro Queue

empty
Console Output
> 1: Start
Step 1 / 6

Async Patterns & The Event Loop

JavaScript evolved from callbacks to Promises to async/await — but all three patterns interact with the event loop differently. Understanding which queue each pattern uses explains why execution order can be surprising.

Async Patterns & The Event Loop

JavaScript evolved from callback hell to clean async/await — but all three patterns use the event loop differently.

CallbacksES5 (2009)

loadUser(1, function(user) {
  loadPosts(user.id, function(posts) {
    loadComments(posts[0].id, function(comments) {
      console.log(comments);
      // "Callback Hell" — deeply nested
    });
  });
});

Event Loop Flow

1loadUser(1, cb1)
2→ Web API: fetch user
3→ cb1 pushed to macrotask queue
4→ cb1 runs, calls loadPosts(id, cb2)
5→ Web API: fetch posts
6→ cb2 → loadComments → cb3

✅ Pros

Simple concept

Works everywhere

❌ Cons

Callback hell / pyramid of doom

Error handling is manual

Hard to compose

Inversion of control — you trust the caller to call your callback

PatternQueue UsedPriority
Callbacks (setTimeout)Macrotask queueLow — runs after microtasks
Promise.then()Microtask queueHigh — runs before macrotasks
async/awaitMicrotask queue (Promises)High — same as Promises

Common Pitfalls

These bugs trip up every JavaScript developer — from setTimeout(fn, 0) not being instant to microtasks starving the main thread. Each one is rooted in a misunderstanding of the event loop.

Common Event Loop Pitfalls

These bugs trip up every JavaScript developer. Understanding the event loop is the key to avoiding them.

Code

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

Actual Output

> A
> D
> C
> B

Why?

setTimeout(fn, 0) doesn't mean "run immediately." It means "add to the macrotask queue as soon as possible." Microtasks (Promise.then) always run before macrotasks. So: A (sync) → D (sync) → C (microtask) → B (macrotask).