DNA⚑ JavaScriptThe Event Loop & Async Execution Model
πŸ¦–DinosaurJavaScriptAsyncBrowserInternals

The Event Loop & Async Execution Model

JavaScript is single-threaded but never blocking. The event loop is the scheduling algorithm behind every setTimeout, every Promise, and every React state update.

The Event Loop & Async Execution Model

Understanding the event loop isn't about memorizing output puzzles. It's about having a scheduling mental model β€” predicting when your code runs relative to rendering, user input, and other async operations.

The Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    JavaScript Runtime                        β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚  Call Stack   β”‚    β”‚       Web APIs / Node APIs       β”‚   β”‚
β”‚  β”‚              β”‚    β”‚  (setTimeout, fetch, DOM events,  β”‚   β”‚
β”‚  β”‚  main()     │───→│   requestAnimationFrame, I/O)     β”‚   β”‚
β”‚  β”‚  fn()       β”‚    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚  β”‚  ...        β”‚                β”‚                          β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜                β”‚                          β”‚
β”‚         β”‚                        β”‚                          β”‚
β”‚    Event Loop                    β–Ό                          β”‚
β”‚    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”‚
β”‚    β”‚  1. Drain Microtask Queue (ALL)                 β”‚      β”‚
β”‚    β”‚     Promise.then, queueMicrotask, MutationObserverβ”‚    β”‚
β”‚    β”‚                                                  β”‚      β”‚
β”‚    β”‚  2. Pick ONE Macrotask                          β”‚      β”‚
β”‚    β”‚     setTimeout, setInterval, I/O callbacks      β”‚      β”‚
β”‚    β”‚                                                  β”‚      β”‚
β”‚    β”‚  3. Render (if needed, ~16ms for 60fps)         β”‚      β”‚
β”‚    β”‚     requestAnimationFrame β†’ Style β†’ Layout      β”‚      β”‚
β”‚    β”‚     β†’ Paint β†’ Composite                         β”‚      β”‚
β”‚    β”‚                                                  β”‚      β”‚
β”‚    β”‚  4. requestIdleCallback (if time remaining)     β”‚      β”‚
β”‚    β”‚                                                  β”‚      β”‚
β”‚    β”‚  β†’ Go to step 1                                 β”‚      β”‚
β”‚    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Microtasks vs Macrotasks

This distinction is the core of event loop understanding:

Microtasks (Higher Priority)Macrotasks (Lower Priority)
Promise.then/catch/finallysetTimeout / setInterval
queueMicrotask()setImmediate (Node)
MutationObserverI/O callbacks
process.nextTick (Node)UI rendering
await continuationsrequestAnimationFrame*

*rAF runs at a specific point in the render cycle, between microtasks and the next macrotask.

The Critical Rule

ALL microtasks drain before the next macrotask or render. This means:

// Microtask chain blocks rendering
Promise.resolve().then(() => {
  // This runs before any setTimeout, before any render
  Promise.resolve().then(() => {
    // This ALSO runs before any setTimeout or render
    // You can chain infinitely β€” it will block everything
  });
});
setTimeout(() => console.log('macro'), 0); // Waits for ALL microtasks

The Complete Execution Order

console.log('1 - sync');
 
setTimeout(() => console.log('2 - macro'), 0);
 
Promise.resolve()
  .then(() => console.log('3 - micro'))
  .then(() => console.log('4 - micro (chained)'));
 
queueMicrotask(() => console.log('5 - micro (queued)'));
 
requestAnimationFrame(() => console.log('6 - rAF'));
 
console.log('7 - sync');

Output:

1 - sync
7 - sync
3 - micro
5 - micro (queued)
4 - micro (chained)
2 - macro
6 - rAF (next render frame)

Why This Order?

  1. Synchronous code runs to completion (1, 7)
  2. Call stack empty β†’ drain microtask queue (3, 5, 4) β€” note: chained .then creates a new microtask
  3. Render step: rAF callbacks run (6)
  4. Pick next macrotask (2)

The Render Pipeline Integration

One frame (~16ms at 60fps):
 
β”œβ”€β”€ Execute macrotask (or microtask flush)
β”œβ”€β”€ Drain microtask queue
β”œβ”€β”€ requestAnimationFrame callbacks
β”œβ”€β”€ Style recalculation
β”œβ”€β”€ Layout
β”œβ”€β”€ Paint
β”œβ”€β”€ Composite
β”œβ”€β”€ requestIdleCallback (if time remains in frame)
└── β†’ Next frame

Why This Matters for UI

// ❌ Blocks rendering β€” microtask loop
function busyMicrotasks() {
  let i = 0;
  function loop() {
    if (i++ < 100000) {
      queueMicrotask(loop); // 100K microtasks β€” no render until done
    }
  }
  loop();
}
 
// βœ… Yields to renderer β€” uses macrotasks
function yieldingWork() {
  let i = 0;
  function chunk() {
    const end = Math.min(i + 1000, 100000);
    while (i < end) processItem(i++);
    if (i < 100000) setTimeout(chunk, 0); // Yields between chunks
  }
  chunk();
}

Timing APIs and When to Use Each

setTimeout(fn, 0) β€” "Do this later"

// Defers to next macrotask β€” after microtasks AND potentially after a render
setTimeout(() => {
  // Good for: breaking up long synchronous work
  // Bad for: anything that needs to happen before next paint
}, 0);

Minimum delay: Browsers clamp nested setTimeout to 4ms after 5 levels of nesting.

queueMicrotask(fn) β€” "Do this ASAP, but after current task"

queueMicrotask(() => {
  // Runs before any macrotask or render
  // Good for: state updates that must be consistent before next render
  // Bad for: heavy work (blocks rendering)
});

requestAnimationFrame(fn) β€” "Do this before next paint"

requestAnimationFrame(() => {
  // Synchronized with display refresh (~60fps)
  // Good for: DOM measurements, animations, visual updates
  // Each callback gets a DOMHighResTimeStamp
  element.style.transform = `translateX(${position}px)`;
});

requestIdleCallback(fn) β€” "Do this when you have free time"

requestIdleCallback((deadline) => {
  while (deadline.timeRemaining() > 0 && tasks.length > 0) {
    processTask(tasks.shift());
  }
  if (tasks.length > 0) {
    requestIdleCallback(processRemainingTasks);
  }
}, { timeout: 2000 }); // Force execution after 2s even if busy

Good for: Analytics, prefetching, non-critical computations.

Comparison

APIPriorityWhen it runsUse for
queueMicrotaskHighestBefore next task/renderCritical state consistency
requestAnimationFrameHighBefore next paintVisual updates, animations
setTimeout(fn, 0)MediumNext macrotaskYielding, deferring work
requestIdleCallbackLowestWhen browser is idleBackground work, analytics

async/await and the Event Loop

await pauses the function and schedules the continuation as a microtask:

async function example() {
  console.log('A');        // Sync
  await Promise.resolve(); // Pauses here, schedules rest as microtask
  console.log('B');        // Runs as microtask
}
 
console.log('1');
example();
console.log('2');
 
// Output: 1, A, 2, B

The code after await is essentially the .then() callback β€” it runs after the current synchronous code completes.

Node.js Differences

Node.js has additional phases:

   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”Œβ”€>β”‚           timers          β”‚  setTimeout, setInterval
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  β”‚     pending callbacks     β”‚  System-level callbacks
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  β”‚        poll (I/O)         β”‚  File, network callbacks
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  β”‚       setImmediate        β”‚  After I/O, before timers
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  β”‚     close callbacks       β”‚  socket.destroy(), etc.
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

process.nextTick runs before any other microtask β€” it's even higher priority than Promise.then.

Common Pitfalls

Microtask Starvation

// This freezes the browser β€” rendering never gets a chance
function infinite() {
  Promise.resolve().then(infinite);
}
infinite(); // ☠️ Blocks ALL macrotasks and rendering

The "setTimeout is not a timer" Misconception

setTimeout(fn, 100); // "Run fn after AT LEAST 100ms, not exactly 100ms"

If the main thread is busy for 200ms, the callback won't run until 200ms+ even though 100ms was specified.

Promise.resolve() vs new Promise(resolve => resolve())

// Both create resolved promises, but:
Promise.resolve(value); // If value is a Promise, returns it as-is (no wrapping)
new Promise(resolve => resolve(value)); // Always creates a new Promise wrapper

Interview Signal

Senior candidates demonstrate:

  1. The full loop β€” Microtasks β†’ render β†’ macrotask, not just "microtask before macrotask"
  2. Render integration β€” Where rAF fits, why microtask floods block painting
  3. Practical scheduling β€” Choosing the right API for the right timing need
  4. Starvation awareness β€” Understanding that microtasks can block everything
  5. Node.js awareness β€” process.nextTick, setImmediate, the poll phase