Ad – 728Γ—90
πŸš€ Advanced JS

JavaScript Event Loop – How JS Handles Asynchronous Code

JavaScript is single-threaded β€” only one piece of code can execute at a time. Yet it handles timers, network requests, and DOM events without blocking. The secret is the event loop: a mechanism that coordinates the call stack, Web APIs, the macrotask queue, and the microtask queue. Understanding this model is essential for predicting execution order, avoiding UI freezes, and passing technical interviews.

⏱️ 22 min read 🎯 Advanced πŸ“… Updated 2026

JavaScript Is Single-Threaded

JavaScript has a single call stack β€” only one function can run at a time. This simplicity means you never have to worry about race conditions between threads. However, it also means long-running synchronous code blocks everything, including the UI. The event loop is how JavaScript achieves concurrency without multiple threads.

ℹ️
The Four Components

1. Call Stack β€” where functions execute (LIFO). 2. Web APIs β€” browser/Node APIs that handle async work off-thread (timers, fetch, DOM events). 3. Macrotask Queue β€” callbacks ready to run after the current stack is empty. 4. Microtask Queue β€” high-priority callbacks (Promises, queueMicrotask) drained entirely before the next macrotask.

The Call Stack

The call stack is a LIFO (Last In, First Out) data structure. When a function is called, a new frame is pushed onto the stack. When the function returns, its frame is popped. If the stack ever overflows (too much recursion), you get a "Maximum call stack size exceeded" error.

JavaScript
function third()  { console.log('third');  }
function second() { third(); console.log('second'); }
function first()  { second(); console.log('first'); }

/*
  Call stack progression:
  [first]          ← push first
  [first, second]  ← push second
  [first, second, third] ← push third
  [first, second]  ← third returns, popped
  [first]          ← second returns, popped
  []               ← first returns, popped
*/

first();
// Output: third β†’ second β†’ first
β–Ά Output
third
second
first

Web APIs – Async Work Off the Stack

When you call setTimeout, fetch, or add an event listener, the browser's Web APIs (or Node.js's libuv) handle the async work outside the JavaScript thread. When the work completes, the callback is placed into a queue β€” not directly back onto the call stack.

JavaScript
console.log('A');                    // 1. Synchronous

setTimeout(() => {
  console.log('B');                  // 4. Macrotask (after ~0ms delay)
}, 0);

fetch('https://jsonplaceholder.typicode.com/todos/1')
  .then(r => r.json())
  .then(data => {
    console.log('C: fetched', data.id); // 5. Microtask (after network)
  });

Promise.resolve().then(() => {
  console.log('D');                  // 3. Microtask (before timeout)
});

console.log('E');                    // 2. Synchronous

// Order: A β†’ E β†’ D β†’ B β†’ C (network response)
β–Ά Output
A
E
D
B
C: fetched 1

The Event Loop Algorithm

The event loop runs this algorithm continuously:

  1. Execute all synchronous code until the call stack is empty.
  2. Drain the microtask queue completely (run all microtasks, including any newly queued by microtasks).
  3. Render the page if needed (browser only).
  4. Take one macrotask from the macrotask queue and execute it.
  5. Go to step 2.
JavaScript
// Execution order quiz β€” predict before running
console.log('1');

setTimeout(() => console.log('2'), 0);           // macrotask
setTimeout(() => console.log('3'), 0);           // macrotask

Promise.resolve()
  .then(() => {
    console.log('4');                            // microtask 1
    return Promise.resolve();
  })
  .then(() => console.log('5'));                 // microtask 2

queueMicrotask(() => console.log('6'));          // microtask (same queue)

console.log('7');

// Answer: 1 β†’ 7 β†’ 4 β†’ 6 β†’ 5 β†’ 2 β†’ 3
β–Ά Output
1
7
4
6
5
2
3

Macrotasks vs Microtasks

QueueSourcesWhen drainedPriority
MicrotaskPromise .then/.catch/.finally, queueMicrotask(), MutationObserverAfter every task β€” all microtasks run before next taskHigher
MacrotasksetTimeout, setInterval, setImmediate (Node), I/O callbacks, UI eventsOne at a time β€” then microtasks drain againLower
⚠️
Infinite microtask loop

If a microtask keeps queuing new microtasks (e.g., a Promise that resolves with another Promise that resolves with another…), the macrotask queue β€” including UI rendering β€” will starve. The page freezes. Always ensure microtask chains terminate.

setTimeout(fn, 0) Explained

setTimeout(fn, 0) does not run fn immediately. It schedules fn as a macrotask to run as soon as possible β€” but only after the current call stack is empty and all microtasks have been drained.

JavaScript
// Use-case: defer heavy work to avoid blocking the UI
function processHeavyData(items) {
  let index = 0;

  function processChunk() {
    const end = Math.min(index + 100, items.length);
    for (; index < end; index++) {
      // process items[index]
    }
    if (index < items.length) {
      setTimeout(processChunk, 0);  // yield to event loop, then continue
    } else {
      console.log('Processing complete');
    }
  }

  setTimeout(processChunk, 0);  // start without blocking caller
}

const bigArray = Array.from({ length: 350 }, (_, i) => i);
processHeavyData(bigArray);
console.log('Caller continues immediately');

// Use-case: ensure DOM is updated before reading layout
// setTimeout(() => console.log(element.offsetHeight), 0);
β–Ά Output
Caller continues immediately
Processing complete

requestAnimationFrame Timing

requestAnimationFrame(callback) schedules a callback to run before the next browser repaint (typically every ~16.7ms at 60 fps). It sits between macrotasks and the render step in the event loop. Use it for animations β€” never setTimeout for visual updates.

JavaScript
// Smooth animation with requestAnimationFrame (browser only)
function animateBox(element, targetX, duration) {
  const startX = 0;
  const startTime = performance.now();

  function frame(currentTime) {
    const elapsed  = currentTime - startTime;
    const progress = Math.min(elapsed / duration, 1);
    // Ease-in-out cubic
    const eased = progress < 0.5
      ? 4 * progress ** 3
      : 1 - (-2 * progress + 2) ** 3 / 2;

    element.style.transform = `translateX(${eased * targetX}px)`;

    if (progress < 1) {
      requestAnimationFrame(frame);   // schedule next frame
    } else {
      console.log('Animation complete');
    }
  }

  requestAnimationFrame(frame);  // kick off first frame
}

// Usage: animateBox(document.querySelector('.box'), 300, 1000);
πŸ’‘
Use queueMicrotask for high-priority deferred work

queueMicrotask(fn) adds fn to the microtask queue explicitly. It is equivalent to Promise.resolve().then(fn) but semantically clearer when you are not actually working with Promises. Use it when you need to defer work until after synchronous code completes but before any macrotask.

Ad – 336Γ—280

πŸ‹οΈ Practical Exercise

Predict the output of this code, then run it to verify:

console.log('start');
setTimeout(() => console.log('timeout 1'), 0);
Promise.resolve().then(() => {
  console.log('microtask 1');
  setTimeout(() => console.log('timeout 2'), 0);
  Promise.resolve().then(() => console.log('microtask 2'));
});
setTimeout(() => console.log('timeout 3'), 0);
console.log('end');

Write your predicted order before revealing the answer.

πŸ”₯ Challenge Exercise

Implement a scheduler object with three methods: asap(fn) (runs as a microtask), defer(fn) (runs as a macrotask via setTimeout 0), and nextFrame(fn) (runs via requestAnimationFrame). Write a test that demonstrates the correct execution order: synchronous code β†’ asap β†’ nextFrame β†’ defer.

Interview Questions

  • Why is JavaScript single-threaded and how does it still handle async operations?
  • What is the difference between the macrotask queue and the microtask queue?
  • Why do Promise callbacks run before setTimeout callbacks with the same delay?
  • What does setTimeout(fn, 0) actually do?
  • What is the event loop algorithm β€” describe it step by step?
  • What is requestAnimationFrame and how does it fit into the event loop?

πŸ“‹ Summary

  • JavaScript is single-threaded: one call stack, one thing running at a time.
  • Web APIs handle async work off the stack; completed callbacks go to queues.
  • The event loop algorithm: run stack β†’ drain all microtasks β†’ render β†’ run one macrotask β†’ repeat.
  • Microtasks (Promises, queueMicrotask) always run before the next macrotask.
  • setTimeout(fn, 0) defers execution to the next macrotask slot β€” not immediately.
  • requestAnimationFrame is the correct tool for browser animations β€” it aligns with repaint cycles.

Frequently Asked Questions

What is the difference between setTimeout(fn, 0) and setImmediate(fn) in Node.js? +

setImmediate(fn) is a Node.js-specific API that places the callback at the head of the check phase of the libuv event loop β€” always after I/O callbacks in the current iteration. setTimeout(fn, 0) queues in the timers phase. In practice, the order between them depends on context: inside an I/O callback, setImmediate always fires first.

What is a "task" vs an "event"? +

A task (macrotask) is a unit of work the event loop picks from the task queue: a timer callback, an I/O callback, a script execution. An event is something that happens (a click, a network response) that the browser translates into a task by adding the event handler callback to the task queue.

Can long microtask chains freeze the browser? +

Yes. Because the microtask queue is fully drained before the browser can render, a microtask that continuously queues new microtasks will prevent any rendering from occurring β€” effectively freezing the UI. Keep microtask chains short and terminating.