Suggest an editImprove this articleRefine the answer for “Event Loop: Microtasks vs Macrotasks”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Event Loop** processes tasks in strict order: synchronous code runs first, then all microtasks (Promise callbacks, `queueMicrotask`) are drained, then one macrotask (`setTimeout`, I/O) runs, then the cycle repeats. ```javascript console.log(1); setTimeout(() => console.log(2), 0); Promise.resolve().then(() => console.log(3)); console.log(4); // Output: 1, 4, 3, 2 ``` **Key:** microtasks always run before the next macrotask, including `setTimeout(..., 0)`.Shown above the full answer for quick recall.Answer (EN)Image**Event Loop** is JavaScript's single-thread task scheduler: it runs all synchronous code first, drains the entire microtask queue (Promises, `queueMicrotask`), optionally renders, then picks one macrotask (`setTimeout`, I/O), and repeats. ## Theory ### TL;DR - Restaurant analogy: one chef (call stack), priority tickets (microtasks), regular orders (macrotasks). All priority tickets clear before the next regular order starts. - Microtasks run as a full batch after every synchronous block. Macrotasks run one at a time, with rendering checks between them. - Microtasks block rendering if they keep queuing each other. A single `setTimeout` does not. - Need the result before the next paint? Microtask. Can it wait a frame? Macrotask. ### Quick example ```javascript console.log('1. Sync'); setTimeout(() => console.log('2. Macrotask'), 0); Promise.resolve() .then(() => console.log('3. Microtask')); console.log('4. Sync'); // Output: // 1. Sync // 4. Sync // 3. Microtask // 2. Macrotask ``` Synchronous code runs first, top to bottom. Then microtasks. Then macrotasks, even with a 0ms delay set on the timer. ### Key difference Microtasks drain completely before anything else moves forward. If a microtask queues another microtask, that one also runs in the same cycle, before any rendering or macrotask. Macrotasks are different: the browser picks exactly one per cycle, checks whether rendering is needed, then checks microtasks again. That is why a hundred chained Promises can freeze a UI, but a `setTimeout(..., 0)` loop will not. ### When to use - **Microtasks:** state updates that must apply before the user sees anything (React batching, Vue reactivity), DOM measurements, input validation that feeds the same render - **Macrotasks:** timers, I/O (fetch, file reading), user events (click, scroll), anything that can safely yield to the browser between frames If the work must finish before the next paint, use a microtask. If it can wait a frame, use a macrotask. ### Comparison table | | Microtasks | Macrotasks | |---|---|---| | **Examples** | `Promise.then`, `queueMicrotask()`, `MutationObserver` | `setTimeout`, `setInterval`, `fetch`, DOM events | | **Execution timing** | After current script, before rendering | One per cycle, after rendering check | | **Blocks rendering** | Yes, if many queue up | No, browser renders between each | | **Queue behavior** | Fully drained each cycle | One task dequeued per cycle | | **When to use** | State changes, DOM reads, validation | Delays, I/O, user interactions | ### How the engine handles this The JavaScript engine keeps two separate queues. After the call stack empties, `MicrotaskQueue::RunMicrotasks()` runs until the queue is empty. Only then does the browser check whether a repaint is needed. Only then does it pull one macrotask. Microtasks are part of the current execution context. Macrotasks represent work arriving from outside. ### Common mistakes **Mistake 1: assuming `setTimeout(..., 0)` runs right away** ```javascript setTimeout(() => console.log('fast'), 0); console.log('slow'); // Output: slow, fast // setTimeout is a macrotask. It always waits for the microtask queue to empty. ``` If you need something to run right after the current script, use `Promise.resolve().then()` or `queueMicrotask()`, not `setTimeout`. **Mistake 2: starving the UI with recursive microtasks** ```javascript // Bad: the browser never gets to render function starveUI() { function loop() { Promise.resolve().then(loop); // queues itself as a microtask } loop(); } // Fix: yield with a macrotask function safeLoop() { function step() { console.log('step'); setTimeout(step, 0); // browser can render between steps } step(); } ``` **Mistake 3: thinking `await` inside a loop yields to the browser** ```javascript // Wrong: awaiting a resolved Promise is still a microtask async function processItems(items) { for (const item of items) { await Promise.resolve(); // microtask, rendering does NOT happen here expensiveCalculation(item); } } // Right: yield with setTimeout so the browser can paint between iterations async function processItems(items) { for (const item of items) { await new Promise(r => setTimeout(r, 0)); // macrotask, browser can render expensiveCalculation(item); } } ``` **Mistake 4: underestimating MutationObserver in tight loops** ```javascript // Bad: 1000 DOM changes = 1000 microtask callbacks, all before any rendering const observer = new MutationObserver(() => updateUI()); for (let i = 0; i < 1000; i++) { element.textContent = i; } // Fix: batch the DOM change into one update element.textContent = 999; // one mutation, one callback ``` **Mistake 5: assuming Node.js and browser behavior match** In browsers and Node.js v11+, microtasks always run before the next macrotask. In Node.js v10 and earlier, timer callbacks ran before microtasks in certain phases of the event loop. If you support older Node environments, verify behavior there rather than assuming it. ### Real-world usage - **React:** batches multiple `setState` calls using `Promise.resolve().then()` internally, so one event handler triggers one render instead of many - **Vue:** reactive updates go through `Promise.then()` for the same batching reason - **Node.js streams:** use macrotasks between data chunks to avoid I/O backpressure blocking the event loop - **Intersection Observer:** callbacks run as macrotasks, so the browser can render between observations - **Express:** async middleware runs in the microtask queue within a single request cycle In practice, the starvation bug shows up in a subtle form: you `await somePromise` inside a loop expecting to yield to the browser, but if that Promise is already resolved, it queues a microtask. Rendering never happens. ### Follow-up questions **Q:** Why do microtasks run before macrotasks? **A:** Microtasks represent work that belongs to the current execution context, like a Promise resolving after synchronous code. Macrotasks represent new work arriving from outside (a timer firing, a network response). Draining microtasks first ensures all dependent async work finishes before the browser handles something new. **Q:** What happens if a microtask queues another microtask? **A:** It runs in the same cycle. The event loop does not move to macrotasks until the microtask queue is completely empty. This is exactly how starvation happens. **Q:** How does `async/await` fit into this? **A:** `async/await` is syntax over Promises. When you hit an `await`, the function pauses and the rest of it is queued as a microtask. The function resumes in the microtask queue, not the macrotask queue. **Q:** What is the difference between `queueMicrotask()` and `Promise.resolve().then()`? **A:** Both add callbacks to the same microtask queue. `queueMicrotask()` does not allocate a Promise object, so it is slightly cheaper. Use it when you want a microtask without needing error handling or chaining. **Q:** (Senior) A React app drops frames during a large data update. Microtask buildup is suspected. How do you fix it? **A:** Move the expensive work to macrotasks with `setTimeout(..., 0)` so the browser can render between batches. In React 18+, wrap non-urgent updates in `startTransition()`, which lets React pause and yield between chunks. For fine-grained control, `requestIdleCallback()` runs work only when the browser has idle time, and `requestAnimationFrame()` ties work to the paint cycle. ## Examples ### Basic: execution order ```javascript console.log('start'); setTimeout(() => console.log('timeout'), 0); Promise.resolve() .then(() => console.log('promise 1')) .then(() => console.log('promise 2')); console.log('end'); // Output: // start // end // promise 1 // promise 2 // timeout ``` Both `.then()` callbacks run before `setTimeout` because each chained `.then()` queues a new microtask right after the previous one resolves. The entire chain drains before the macrotask gets a turn. ### Intermediate: React state batching ```javascript function handleClick() { setCount(c => c + 1); // queued as microtask setCount(c => c + 1); // queued as microtask console.log('handler done'); // state has not updated yet at this point } // React collects both updates in the microtask queue, // then applies them together after the handler finishes. // Result: one re-render, not two. ``` React uses `Promise.resolve().then()` internally to collect all state changes from a single event and apply them in one render pass. Without this batching, each `setCount` call would trigger a separate render. ### Senior: starvation vs proper yielding ```javascript // Bad: freezes the browser completely function starve() { function tick() { Promise.resolve().then(tick); } tick(); // Each microtask queues the next one. // Rendering never happens. } // Better: yields to the browser between iterations function safeYield() { function tick() { // do some work setTimeout(tick, 0); // browser can repaint between calls } tick(); } // React 18+: startTransition for non-urgent updates import { startTransition } from 'react'; startTransition(() => { setItems(buildLargeList()); // React can pause and render intermediate state }); ``` `startTransition` marks the update as non-urgent. React can interrupt the work mid-way, render what is ready, and continue. The page stays responsive instead of freezing for the full computation.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.