Event Loop & Concurrency

Call stack, task queue, microtasks, and how JavaScript handles asynchronous operations

How JavaScript Handles Async Operations

JavaScript is single-threaded, meaning it can only execute one piece of code at a time. Yet it handles async operations like network requests and timers seamlessly. This is possible thanks to the event loop, which coordinates execution between the call stack and task queues.

Key Components

  • Call Stack — Where JavaScript executes functions (LIFO)
  • Web APIs — Browser-provided APIs (setTimeout, fetch, DOM events)
  • Task Queue (Macrotask) — Holds callbacks from setTimeout, setInterval, I/O
  • Microtask Queue — Holds Promise callbacks, queueMicrotask, MutationObserver
  • Event Loop — Continuously checks if stack is empty, then processes queues

The Call Stack

JavaScript uses a call stack to track function execution:

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(4)
// 2. printSquare(4) → square(4)
// 3. printSquare(4) → square(4) → multiply(4, 4)
// 4. printSquare(4) → square(4) ← returns 16
// 5. printSquare(4) ← returns 16
// 6. Empty (done)

setTimeout and the Task Queue

console.log("Start");

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

console.log("End");

// Output:
// "Start"
// "End"
// "Timeout"

// Why? Even with 0ms delay:
// 1. console.log("Start") runs
// 2. setTimeout callback → Task Queue (not call stack)
// 3. console.log("End") runs
// 4. Stack empty → Event loop checks Task Queue
// 5. Callback runs → "Timeout"

Microtasks vs Macrotasks

Microtasks have higher priority than macrotasks (tasks). All microtasks run before the next macrotask:

console.log("1. Script start");

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

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

console.log("2. Script end");

// Output order:
// 1. Script start
// 2. Script end
// 3. Promise (microtask)
// 4. setTimeout (macrotask)
Microtasks (High Priority) Macrotasks (Lower Priority)
Promise.then/catch/finally setTimeout
queueMicrotask() setInterval
MutationObserver setImmediate (Node)
async/await (after await) I/O operations

Event Loop Algorithm

// Simplified Event Loop
while (true) {
  // 1. Execute all synchronous code in call stack
  
  // 2. Execute ALL microtasks
  while (microtaskQueue.length > 0) {
    executeMicrotask(microtaskQueue.shift());
  }
  
  // 3. Execute ONE macrotask (if any)
  if (macrotaskQueue.length > 0) {
    executeMacrotask(macrotaskQueue.shift());
  }
  
  // 4. Render updates (if needed, ~60fps)
  
  // 5. Repeat
}

Complex Example

console.log("1");

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

Promise.resolve()
  .then(() => {
    console.log("3");
    setTimeout(() => console.log("4"), 0);
  })
  .then(() => console.log("5"));

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

console.log("7");

// Output: 1, 7, 3, 6, 5, 2, 4

// Explanation:
// Sync: 1, 7
// Microtasks: 3, 6, 5 (Promise chains)
// Macrotask: 2 (first setTimeout)
// Macrotask: 4 (setTimeout added during microtask)

async/await and the Event Loop

async function asyncExample() {
  console.log("1. Async start");
  
  await Promise.resolve();
  // Everything after await is a microtask
  
  console.log("3. After await");
}

console.log("0. Script start");
asyncExample();
console.log("2. Script end");

// Output:
// 0. Script start
// 1. Async start
// 2. Script end
// 3. After await

// The code after 'await' runs as a microtask

Blocking the Event Loop

// ❌ BAD: Long-running synchronous code blocks everything
function blockLoop() {
  const start = Date.now();
  while (Date.now() - start < 3000) {
    // Blocks for 3 seconds
  }
  console.log("Done blocking");
}

// UI freezes, no callbacks run, no rendering
blockLoop();

// ✅ BETTER: Break up work
function processChunk(items, index = 0, chunkSize = 100) {
  const end = Math.min(index + chunkSize, items.length);
  
  for (let i = index; i < end; i++) {
    // Process item
  }
  
  if (end < items.length) {
    // Yield to event loop, then continue
    setTimeout(() => processChunk(items, end, chunkSize), 0);
  }
}

requestAnimationFrame

// Runs before the next repaint (~60fps)
function animate() {
  // Update animation state
  element.style.left = position + "px";
  
  // Schedule next frame
  requestAnimationFrame(animate);
}


  
requestAnimationFrame(animate);

// Priority: Microtasks → rAF → Macrotasks

💡 Key Takeaways

  • • JavaScript is single-threaded but handles async via the event loop
  • • Microtasks (Promises) run before macrotasks (setTimeout)
  • • All microtasks run before the next macrotask
  • • Long synchronous operations block the event loop and freeze the UI
  • • Use setTimeout or requestAnimationFrame to break up heavy work
  • • async/await code after await runs as a microtask