Introduction

JavaScript is single-threaded, yet it handles asynchronous operations beautifully. The Event Loop is the mechanism that makes this possible. Understanding it is crucial for writing efficient JavaScript code.

The Single-Threaded Model

What Does Single-Threaded Mean?

JavaScript executes code one line at a time in a single execution context:

console.log("1");
console.log("2");
console.log("3");
// Always executes in order: 1, 2, 3

But wait… How does it handle:

  • setTimeout
  • fetch requests
  • DOM events
  • File I/O

Answer: The Event Loop!

Event Loop Architecture

Components

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚         Call Stack                  β”‚
β”‚  (Main thread - synchronous code)   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              β”‚
              ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚         Web APIs                    β”‚
β”‚  (Browser APIs - async operations)  β”‚
β”‚  - setTimeout                       β”‚
β”‚  - fetch                            β”‚
β”‚  - DOM events                       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              β”‚
              ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚    Callback Queue (Task Queue)      β”‚
β”‚  (Ready callbacks waiting to run)   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              β”‚
              ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚    Microtask Queue                  β”‚
β”‚  (Promise callbacks, queueMicrotask)β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

How It Works

Step-by-Step Execution

console.log("1");

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

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

console.log("4");

// Output: 1, 4, 3, 2

Execution flow:

  1. console.log('1') β†’ Call Stack β†’ Executes β†’ Outputs β€œ1”
  2. setTimeout(...) β†’ Call Stack β†’ Web APIs β†’ Timer starts
  3. Promise.resolve().then(...) β†’ Call Stack β†’ Microtask Queue
  4. console.log('4') β†’ Call Stack β†’ Executes β†’ Outputs β€œ4”
  5. Call Stack empty β†’ Check Microtask Queue β†’ Execute Promise β†’ Outputs β€œ3”
  6. Check Callback Queue β†’ Execute setTimeout β†’ Outputs β€œ2”

Key Rules

  1. Call Stack must be empty before callbacks run
  2. Microtasks run before Macrotasks
  3. Microtasks include: Promises, queueMicrotask, MutationObserver
  4. Macrotasks include: setTimeout, setInterval, I/O, UI rendering

Call Stack

The call stack is where synchronous code executes:

function a() {
  console.log("a");
  b();
}

function b() {
  console.log("b");
  c();
}

function c() {
  console.log("c");
}

a();

// Call Stack:
// [a] β†’ [a, b] β†’ [a, b, c] β†’ [a, b] β†’ [a] β†’ []

Stack Overflow

function infinite() {
  infinite(); // Recursive call
}

infinite();
// RangeError: Maximum call stack size exceeded

Web APIs

Browser APIs handle asynchronous operations outside the main thread:

// setTimeout - Timer API
setTimeout(() => {
  console.log("After 1 second");
}, 1000);

// fetch - Network API
fetch("/api/data")
  .then((res) => res.json())
  .then((data) => console.log(data));

// addEventListener - DOM API
button.addEventListener("click", () => {
  console.log("Clicked!");
});

These APIs run in separate threads, then queue callbacks when complete.

Callback Queue (Task Queue)

Also called the β€œMacrotask Queue” - handles:

  • setTimeout/setInterval callbacks
  • I/O operations
  • UI rendering
console.log("Start");

setTimeout(() => {
  console.log("Timeout 1");
}, 0);

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

console.log("End");

// Output: Start, End, Timeout 1, Timeout 2

Microtask Queue

Higher priority than callback queue - handles:

  • Promise callbacks (.then, .catch, .finally)
  • queueMicrotask()
  • MutationObserver
console.log("1");

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

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

queueMicrotask(() => console.log("4"));

console.log("5");

// Output: 1, 5, 3, 4, 2
// Microtasks (3, 4) run before macrotask (2)

Promises and the Event Loop

Promise Execution

console.log("Start");

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

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

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

console.log("End");

// Output:
// Start
// End
// Promise 1
// Promise 2
// Promise 3
// Timeout

Why? All Promise callbacks go to Microtask Queue and execute before Callback Queue.

Async/Await

Async/await is syntactic sugar over Promises:

async function example() {
  console.log("1");

  await Promise.resolve();
  console.log("2");

  await Promise.resolve();
  console.log("3");
}

console.log("Start");
example();
console.log("End");

// Output: Start, 1, End, 2, 3

Behind the scenes:

  • await pauses function execution
  • Rest of function becomes Promise callback
  • Goes to Microtask Queue

Common Patterns

Blocking the Event Loop

// ❌ Bad - blocks event loop
function heavyComputation() {
  for (let i = 0; i < 1000000000; i++) {
    // Long computation
  }
}

heavyComputation();
// Browser freezes - can't process events!

// βœ… Good - yields to event loop
async function heavyComputation() {
  for (let i = 0; i < 1000000000; i++) {
    if (i % 1000000 === 0) {
      await new Promise((resolve) => setTimeout(resolve, 0));
    }
  }
}

Race Conditions

let value = 0;

// Both modify value
setTimeout(() => value++, 0);
setTimeout(() => value++, 0);

// Result depends on timing!

Best Practices

βœ… Do This

1. Use async/await for readability

async function fetchData() {
  const response = await fetch("/api/data");
  const data = await response.json();
  return data;
}

2. Break up long computations

async function processLargeArray(arr) {
  for (let i = 0; i < arr.length; i += 1000) {
    processChunk(arr.slice(i, i + 1000));
    await new Promise((resolve) => setTimeout(resolve, 0));
  }
}

3. Use requestAnimationFrame for animations

function animate() {
  // Update animation
  requestAnimationFrame(animate);
}
animate();

❌ Don’t Do This

1. Don’t block the event loop

// ❌ Blocks everything
while (true) {
  // Infinite loop
}

// ❌ Heavy synchronous computation
for (let i = 0; i < 1e9; i++) {
  // Long calculation
}

2. Don’t create callback hell

// ❌ Nested callbacks
getData((data) => {
  processData(data, (result) => {
    saveData(result, (saved) => {
      // Too nested!
    });
  });
});

// βœ… Use async/await
async function workflow() {
  const data = await getData();
  const result = await processData(data);
  await saveData(result);
}

Visualizing the Event Loop

Use our tools:

Conclusion

The Event Loop is fundamental to JavaScript:

Key takeaways:

  • JavaScript is single-threaded
  • Event Loop enables async operations
  • Microtasks run before macrotasks
  • Don’t block the event loop
  • Use async/await for cleaner code

Understanding the Event Loop helps you:

  • Debug async issues
  • Write performant code
  • Avoid blocking the UI
  • Predict execution order

Next Steps