Skip to Content
Course content

57: Microtasks vs Macrotasks

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was reviewing a PR for a junior dev who was building a complex data dashboard. He had a bug where a "Loading" spinner would occasionally stay on the screen for a fraction of a second longer than it should, even though the data had technically arrived. He had wrapped the "hide spinner" logic in a setTimeout(() => { ... }, 0), thinking he was just pushing it to the end of the line to ensure the DOM had updated. But the data fetching was handled by a Promise. To his frustration, the spinner remained visible while other Promise-based updates were firing immediately. He kept increasing the timeout to 10ms, then 50ms, trying to "outrun" the bug without realizing he was fighting the very architecture of the JavaScript engine.

This happens because JavaScript doesn't treat all asynchronous tasks as equals. When we talk about the Event Loop, we often simplify it to "stuff that happens later," but there's actually a strict hierarchy at play: Macrotasks and Microtasks.

The Hidden Priority Queue

Think of the Macrotask queue as the main lobby of a building. This is where setTimeout, setInterval, and I/O operations wait their turn. Now, imagine a VIP side-door—that's the Microtask queue. This queue is reserved for things like Promise.then() callbacks, MutationObserver, and queueMicrotask().

Here is the crucial part: the Event Loop will not move on to the next Macrotask until the Microtask queue is completely empty. If a microtask schedules another microtask, that new one also gets processed in the same cycle. This is why Promises always seem to "jump the line" ahead of timers.

console.log('1: Script start');

setTimeout(() => {
  console.log('2: setTimeout (Macrotask)');
}, 0);

Promise.resolve().then(() => {
  console.log('3: Promise 1 (Microtask)');
}).then(() => {
  console.log('4: Promise 2 (Microtask)');
});

console.log('5: Script end');

If you run this, you'll see 1, 5, 3, 4, and finally 2. Even though the timeout was set to 0ms, the engine finished the synchronous script, cleared out every single pending microtask (both the first and second .then()), and only then looked back at the macrotask queue to find the timeout.

The Danger of Microtask Starvation

Since the engine refuses to move to the next macrotask until the microtask queue is empty, you can actually crash your browser or freeze your UI if you aren't careful. I've seen developers accidentally create an infinite loop of microtasks—for example, a recursive function that schedules itself via queueMicrotask.

Unlike a setTimeout recursion, which allows the browser to breathe, render the page, and handle clicks between calls, a microtask loop blocks everything. The browser can't repaint the screen because it's still stuck in the "VIP side-door" trying to clear the microtask queue. If you find your UI freezing despite using asynchronous code, check if you're flooding the microtask queue.

In short: use Promises and microtasks for things that need to happen immediately after the current execution but before the browser renders. Use setTimeout when you actually want to yield control back to the browser to let it handle other events or paint the screen.




📋 Practical Task

The Async Execution Race Debugger

Below is a snippet of code intended to synchronize a state update and a logging mechanism. However, the logs are appearing in an unexpected order, causing the developer to think the state isn't updating before the log fires. Your task is to rewrite the logStatus function using queueMicrotask instead of setTimeout to ensure the log happens after the Promise resolves but before the browser handles any other pending macrotasks.


const state = { loading: true };

function updateState() {
  return new Promise((resolve) => {
    console.log("Updating state...");
    state.loading = false;
    resolve();
  });
}

function logStatus() {
  // CURRENTLY: This is a macrotask
  setTimeout(() => {
    console.log(`State loading: ${state.loading}`);
  }, 0);
}

// Execution
updateState().then(() => {
  console.log("State update Promise resolved.");
  logStatus();
});

console.log("Script execution finished.");

// Expected output order after your fix:
// 1. Updating state...
// 2. Script execution finished.
// 3. State update Promise resolved.
// 4. State loading: false

Instructions: Modify the logStatus function to ensure the log is queued as a microtask. Verify that the output matches the expected order provided in the comments.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.