f in x
JavaScript Event Loop, Microtasks and Macrotasks — How Concurrency Becomes Execution Order
> cd .. / HUB_EDITORIALE
Sviluppo di siti web

JavaScript Event Loop, Microtasks and Macrotasks — How Concurrency Becomes Execution Order

[2026-07-10] Author: Ing. Calogero Bono
Zenithby Meteora Web The operating system for your business. Social, clients, bookings and invoices in one platform. Gyms, barbers, professionals. Discover Zenith Free demo · no card

Your JavaScript code runs in order—until it hits an async operation. An API call, a timer, a Promise, and the order you thought was solid breaks. If you don't understand the Event Loop, every async bug turns into a mystery that costs you hours.

We, at Meteora Web, work daily on real JavaScript applications – custom platforms in Laravel/Vue, e-commerce with React, automation tools. And we see it: developers who write clean synchronous code fall apart when dealing with async. Not because it's complicated, but because they lack the mental map. This guide gives it to you.

What is the Event Loop and why does it determine execution order?

JavaScript is single-threaded: one Call Stack, one thing at a time. But the browser and Node.js are not single-threaded – they have threads for I/O, timers, network. The Event Loop is the bridge between your synchronous code and those external operations. When the Call Stack is empty, the Event Loop decides what goes into it – and in what order.

It's not magic. It's a cycle:

  1. Execute everything in the Call Stack until it's empty.
  2. Look at the Microtask Queue: drain it completely.
  3. Then look at the Macrotask Queue: take the first task and execute it.
  4. Repeat.

If you don't keep this cycle in mind, the timing of setTimeout and Promise remains a guessing game.

Sponsored Protocol

What you can do now: Open your browser or Node.js console. Run:

console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// Output: 1, 4, 3, 2

Result: 1, 4, 3, 2. Why? The Promise (microtask) runs before the setTimeout (macrotask). If you don't know why, keep reading.

How do Call Stack, Microtask Queue, and Macrotask Queue work?

Call Stack – the starting point

The Call Stack is a LIFO stack. Each function call creates a frame. When a function returns, its frame is removed. As long as the stack is not empty, the Event Loop doesn't intervene. A function taking 5 seconds blocks everything – scrolling, clicks, animations. We see it in projects that come to us: a heavy loop on an e-commerce page and the user thinks the site is broken.

Microtask Queue – highest priority

Microtasks include:

  • Promise.then / catch / finally
  • queueMicrotask()
  • MutationObserver (browser)
  • process.nextTick (Node.js)

They execute right after every Call Stack operation, before the Event Loop looks at macrotasks. A microtask can spawn more microtasks, and it drains them all before moving to the next macrotask. That's why a Promise.then() gets priority over setTimeout(fn, 0).

Sponsored Protocol

Macrotask Queue – the next turn

Macrotasks include:

  • setTimeout / setInterval
  • setImmediate (Node.js)
  • DOM events (click, load, scroll) – handled as macrotasks by the browser
  • fetch / XMLHttpRequest – the response arrives as a macrotask

The Event Loop picks one macrotask per cycle. After executing it, it checks microtasks again. This mechanism ensures microtasks don't starve macrotasks, but they do delay them until the microtask queue is empty.

What you can do now: Write this test:

console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => {
  console.log('C');
  queueMicrotask(() => console.log('D'));
});
console.log('E');
// Output: A, E, C, D, B

D (microtask spawned from another microtask) prints before B (macrotask).

What is the real execution order between microtasks and macrotasks with concrete examples?

The most common mistake: believing that setTimeout(fn, 0) runs immediately after the next line. False. Here's the visual:

Sponsored Protocol

  • Synchronous code in the Call Stack
  • When it finishes, all queued microtasks
  • Then one macrotask
  • Then microtasks again
  • And so on

Imagine a real scenario: an e-commerce site must fetch product data (API) and then immediately show a discount popup. If you write:

fetch('/api/prodotti').then(res => res.json()).then(prodotti => aggiornaCarrello(prodotti));
setTimeout(() => mostraPopup(), 0);

The popup appears only after all .then() chains of the fetch have executed – even if the response arrives in 2 ms. Because fetch returns a Promise; .then() is a microtask. setTimeout is a macrotask. So even if the Promise resolves immediately, the setTimeout waits until all microtasks (including those internal to the chain) are done.

How to handle it in practice? If you want the popup to appear as soon as possible, move the popup logic into a microtask or use requestAnimationFrame for rendering. But careful: overusing microtasks can delay painting (microtasks block the browser's render step).

What you can do now: Analyze the async code in your application. Look for patterns like:

setTimeout(() => {
  fetch('/api/data').then(...);
}, 0);

You likely wanted to execute fetch immediately, but the setTimeout delays it unnecessarily. Replace with:

Sponsored Protocol

queueMicrotask(() => fetch('/api/data').then(...));
// or simply fetch outside the timeout

How to avoid Call Stack blocking and memory leaks?

A full Call Stack isn't just slow – it blocks the UI and prevents event handling. A recursive function without an exit condition? Immediate stack overflow. But the more insidious problem is long synchronous loops: a heavy computation occupying the Call Stack for seconds. The Event Loop can't process microtasks or macrotasks. The user clicks and nothing happens.

Solution: split the work into chunks using setTimeout or requestIdleCallback. Example:

function processBatch(items, index) {
  const chunk = items.slice(index, index + 1000);
  chunk.forEach(processItem);
  if (index + 1000 < items.length) {
    setTimeout(() => processBatch(items, index + 1000), 0);
  }
}
processBatch(largeArray, 0);

This way, after each chunk, the Call Stack empties and the Event Loop can handle other tasks (clicks, animations, Promises).

Memory leaks in microtasks: Beware of creating infinite microtasks. If a .then() spawns another Promise that resolves immediately, you create a microtask loop that never yields to macrotasks. The app looks alive but stops responding to external events (timers, scroll). We've seen this bug in production.

Sponsored Protocol

What you can do now: Check your codebase for recursive Promises or .then() chains with no break condition. Add a depth limit or an external timeout to escape.

What to do next

  1. Test your understanding: Open the console and run the first example. Explain the output order out loud. If you hesitate, you need more practice.
  2. Audit your async code: Find all setTimeout with 0 delay and ask: “Is this really needed? Could I use a Promise or queueMicrotask instead?”
  3. Avoid long synchronous blocks: If you have loops processing thousands of items, break them into chunks.
  4. Verify microtasks don't starve macrotasks: Check for infinite Promise recursion.
  5. Dive into official docs: Read the MDN Event Loop article for browser and Node.js Event Loop guide for server-side.
  6. Check our related content: For a broader foundation, see our pillar guide on JavaScript ES2024+.
Ing. Calogero Bono

> AUTHOR_EXTRACTED

Ing. Calogero Bono

Ingegnere informatico, fondatore di Meteora Web e Zenith OS. System administrator e progettista di piattaforme, app e CMS proprietari, con esperienza in sviluppo full-stack, marketing digitale ed ecosistema Google.
[ Read Full Dossier ]

> METEORA_WEB // DIGITAL AGENCY

We build the digital presence your business deserves.

Websites, social media, online advertising, e-commerce and high-performance hosting, engineered with method by computer engineers in Sciacca, for all of Italy.

> MW_JOURNAL

> READ_ALL()