JavaScript Event Loop

Learn how the call stack, microtask queue, and task queue control JavaScript execution order.

TL;DR

  1. 01The call stack executes synchronous code one frame at a time.
  2. 02Microtasks execute immediately after the current stack frame completes.
  3. 03Promises resolve before macrotasks like setTimeout callbacks get processed.

Tips

  1. 01Use the native queueMicrotask() method to run scripts immediately after synchronous blocks but before paint updates.
  2. 02Split heavy data processing into smaller setTimeout batches to give browsers time to render frames.

Warnings

  1. 01Running a long synchronous loop blocks the single execution thread, freezing page interactions and rendering updates.
  2. 02Remember that recursive promise microtasks can starve macrotasks by preventing the event loop from advancing.

The Call Stack

    Call stack frames

    Tracks function calls in progress, stacking execution frames until returns occur.

    function a() { b(); }
    function b() { console.log("b"); }
    a(); // pushes a, then b, then pops
    Single execution thread

    Allows only one stack block to run at any single time.

    // Synchronous execution runs sequentially
    Sync completeness

    Processes synchronous scripts completely before checking background event queues.

    console.log(1);
    console.log(2);
    // 1 always logs before 2
    Stack overflow

    Triggers errors when unbounded recursive calls consume all stack memory.

    function recurse() { return recurse(); }
    // recurse(); // Maximum stack exceeded

Macrotasks and the Task Queue

    Macrotask callbacks

    Includes timer callbacks, network input, and user action triggers.

    setTimeout(() => console.log("macro"), 0);
    console.log("sync");
    Single task turns

    Executes exactly one macrotask per event loop rotation iteration.

    setTimeout(() => console.log("1"), 0);
    setTimeout(() => console.log("2"), 0);
    // separate event loop iterations
    Browser painting

    Interleaves browser paint updates between subsequent task queue turns.

    // Repaints occur after a macrotask completes

Microtasks and Promises

    Promise microtasks

    Runs resolve, reject, and finally callbacks inside the microtask queue.

    Promise.resolve().then(() => console.log("micro"));
    Direct queueing

    Schedules low-level microtasks directly using the queueMicrotask method.

    queueMicrotask(() => console.log("fast"));
    Full queue draining

    Forces all queued microtasks to run before the loop advances.

    Promise.resolve().then(() => console.log("m1"));
    Promise.resolve().then(() => console.log("m2"));
    // m1 and m2 execute back-to-back
    Nested microtasks

    Processes nested microtasks inside the same loop iteration drain phase.

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

setTimeout vs Promise Ordering

    Order priority

    Executes promise resolutions before timers scheduled within the same block.

    setTimeout(() => console.log("time"), 0);
    Promise.resolve().then(() => console.log("prom"));
    // logs: prom, time
    Loop interleaving

    Runs all pending microtasks before processing any scheduled timeout callbacks.

    setTimeout(() => console.log("time"), 0);
    [1, 2].forEach(n => {
      Promise.resolve().then(() => console.log(n));
    });
    // logs: 1, 2, time
    rAF visual timing

    Runs animation callbacks before paint updates, outside standard queues.

    requestAnimationFrame(() => console.log("paint"));

UI Responsiveness

    Execution chunking

    Schedules iteration batches with setTimeout to avoid locking layout repaints.

    function chunk(items, index = 0) {
      const end = Math.min(index + 100, items.length);
      for (let i = index; i < end; i++) { /* do work */ }
      if (end < items.length) {
        setTimeout(() => chunk(items, end), 0);
      }
    }
    Web Workers

    Delegates heavy computations to separate threads away from main render loops.

    const worker = new Worker("task.js");
    worker.postMessage(data);
    worker.onmessage = e => console.log(e.data);

In Practice

FAQ