Fundamentals 12 min read

Understanding the JavaScript Event Loop: Macro‑tasks, Micro‑tasks, Browser and Node.js

This article explains how JavaScript's single‑threaded engine uses the event loop to manage synchronous and asynchronous tasks, distinguishes macro‑tasks from micro‑tasks, and compares the implementations in browsers and Node.js, including version‑specific behavior and common pitfalls.

ByteFE
ByteFE
ByteFE
Understanding the JavaScript Event Loop: Macro‑tasks, Micro‑tasks, Browser and Node.js

Before diving into the event loop, it is useful to know that the JavaScript engine runs on a single thread, meaning it can only perform one operation at a time, unlike multi‑threaded languages such as Java.

JavaScript tasks are classified as synchronous (executed continuously) or asynchronous (split into separate phases with a callback). Typical asynchronous operations include file reads and network requests, which would otherwise leave the engine idle.

If no special handling is applied, asynchronous tasks would cause long periods of wasted idle time. In single‑threaded JavaScript this is unacceptable, so a callback‑notification model is used: while waiting for an async operation, the engine continues executing other synchronous code, and when the async result is ready, the callback is handed to the event loop for execution.

The event loop is a queue‑based mechanism that processes callbacks in a first‑in‑first‑out order. Tasks in the queue are divided into macro‑tasks and micro‑tasks.

Macro‑tasks and Micro‑tasks

The event loop consists of a macro‑task and all micro‑tasks generated while the macro‑task is running. After a macro‑task finishes, all micro‑tasks queued during its execution are processed immediately, giving them a chance to “cut in line” before the next macro‑task.

Common macro‑tasks include script, setTimeout, setInterval, setImmediate (Node‑only), requestAnimationFrame (browser‑only), and various I/O or UI render tasks. Common micro‑tasks are process.nextTick (Node‑only), Promise.then, Object.observe, and MutationObserver.

Misconceptions about setTimeout

The callback of setTimeout is not guaranteed to run exactly after the specified delay; after the delay the callback is simply placed into the event‑loop queue. If the engine is busy with other synchronous work or other queued callbacks, the setTimeout callback will wait. Moreover, a timeout of 0 ms still has a minimum delay of about 4 ms.

setTimeout(() => {
  console.log('setTimeout');
}, 0);
setImmediate(() => {
  console.log('setImmediate');
});

When the global script finishes, if the elapsed time exceeds the 4 ms minimum, the setTimeout callback will be queued first; otherwise setImmediate runs first.

Browser Event Loop

In browsers the event loop consists of a single macro‑task queue and one (or more) micro‑task queues. The first macro‑task is the global script; any macro‑ or micro‑tasks created during its execution are queued. After the script finishes, the current micro‑task queue is emptied, completing one loop iteration. The process then repeats with the next macro‑task.

Demo showing micro‑task priority:

Promise.resolve().then(() => {
  console.log('Micro‑task 1');
  setTimeout(() => {
    console.log('Macro‑task 2');
  }, 0);
});
setTimeout(() => {
  console.log('Macro‑task 1');
  Promise.resolve().then(() => {
    console.log('Micro‑task 2');
  });
}, 0);
// Expected output order:
// Micro‑task 1
// Macro‑task 1
// Micro‑task 2
// Macro‑task 2

Node.js Event Loop

Node’s event loop is more complex, comprising six macro‑task queues (Timers, I/O callbacks, idle, prepare, poll, check, close) each paired with its own micro‑task queue. The macro‑tasks are processed by priority, and after a macro‑task queue is emptied, its micro‑task queue is drained before moving to the next priority level.

Important phases for most user code are Timers ( setTimeout / setInterval), Poll (I/O), and Check ( setImmediate).

Node micro‑tasks have their own priority: process.nextTick runs before any other micro‑tasks such as promises.

console.log('Script start');
setTimeout(() => {
  console.log('Macro‑task 1 (setTimeout)');
  Promise.resolve().then(() => {
    console.log('Micro‑task promise2');
  });
}, 0);
setTimeout(() => {
  console.log('Macro‑task 2 (setTimeout)');
  Promise.resolve().then(() => {
    console.log('Micro‑task promise3');
  });
}, 0);
Promise.resolve().then(() => {
  console.log('Micro‑task promise1');
});
console.log('Script end');
process.nextTick(() => {
  console.log('Micro‑task nextTick');
});

Running this on Node <11 yields the order:

Script start
Script end
Micro‑task nextTick
Micro‑task promise1
Macro‑task 1 (setTimeout)
Macro‑task 3 (setTimeout)
Micro‑task promise2
Macro‑task 2 (setImmediate)

On Node ≥11 the loop behaves more like browsers: after each macro‑task, the current micro‑task queue is drained before the next macro‑task.

Script start
Script end
Micro‑task nextTick
Micro‑task promise1
Macro‑task 1 (setTimeout)
Micro‑task promise2
Macro‑task 3 (setTimeout)
Macro‑task 2 (setImmediate)

Key observations:

In the micro‑task queue, process.nextTick always runs before promise callbacks.

Among macro‑tasks, setTimeout has higher priority than setImmediate.

Before Node 11, micro‑tasks waited for the entire current macro‑task priority level to finish; after Node 11 they run after each individual macro‑task.

Conclusion

Dividing the event loop into macro‑tasks and micro‑tasks gives high‑priority work a chance to “cut in line”: micro‑tasks are executed before the next macro‑task. Node’s loop is more intricate than the browser’s, with six macro‑task priorities and two micro‑task priorities, but since Node 11 the overall behavior has become closer to the browser model.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaScriptNode.jsBrowserevent loopMacro TaskMicro Task
ByteFE
Written by

ByteFE

Cutting‑edge tech, article sharing, and practical insights from the ByteDance frontend team.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.