The Secret Life of JavaScript: The Promise (Microtasks)

Published: (February 2, 2026 at 11:17 PM EST)
3 min read
Source: Dev.to

Source: Dev.to

Understanding the VIP Line: Microtasks vs. Macrotasks

Timothy sat at a small library table, shuffling two pieces of paper. He looked up as Margaret approached, a frown creasing his forehead.

“I don’t get it,” he said quietly. “I told the code to wait zero milliseconds. Zero. That should be instant, right?”

He slid a code snippet across the table.

console.log("1. Start");

setTimeout(() => {
    console.log("2. Timeout");
}, 0);

Promise.resolve().then(() => {
    console.log("3. Promise");
});

console.log("4. End");

“I expected it to go 1, 2, 3, 4,” Timothy explained. “Or maybe 1, 4, 2, 3. But look what actually happened.”

1. Start
4. End
3. Promise
2. Timeout

“Do you remember when we talked about the Event Loop back in Volume 6? We said the Waiter checks the queue.”

“Right,” Timothy nodded.

“Well,” Margaret whispered, “I didn’t tell you the whole story. There isn’t just one queue. There are two.”

She drew a large circle on the chalkboard and labeled it The Event Loop, then added two separate boxes:

  • The Macrotask Queue – the standard line. Holds setTimeout, setInterval, user interactions, etc.
  • The Microtask Queue – the VIP line. Holds Promises, queueMicrotask, and MutationObserver.

“The Engine has a strict rule,” she explained. “When the current task (like your main script) finishes, the Waiter doesn’t just grab the next macrotask. First, he checks the VIP line.”

How the Queues Are Processed

  1. Run all microtasks in the Microtask Queue.
  2. Only then pick the next macrotask from the Macrotask Queue.

Because of this rule, microtasks always run before any pending macrotasks, even if a macrotask’s timer is set to 0 ms.

Execution Order Explained

  • console.log("1. Start") and console.log("4. End") run immediately on the call stack (the current task).
  • setTimeout(..., 0) is placed in the macrotask queue.
  • Promise.resolve().then(...) is placed in the microtask queue.

When the main script finishes, the event loop processes the microtask queue first, producing “3. Promise”, then moves on to the macrotask queue, producing “2. Timeout”.

Nested Microtasks and Starvation

Margaret illustrated a potential pitfall with a nested example:

Promise.resolve().then(() => {
    console.log("VIP 1");
    Promise.resolve().then(() => {
        console.log("VIP 2 (Nested)");
        // If we keep adding Promises here...
    });
});

setTimeout(() => console.log("Standard Line"), 0);

In this case:

  1. “VIP 1” is logged.
  2. The nested Promise.resolve().then adds another microtask (“VIP 2”) to the front of the microtask queue.
  3. The event loop continues processing microtasks until the queue is empty.

If code keeps queuing new microtasks, the microtask queue never empties, and the event loop never reaches the macrotask queue. This situation is known as starvation—the standard line (macrotasks) is starved of execution time, potentially freezing the browser.

Best Practices

  • Use Promises (microtasks) for quick, urgent updates that must happen before the next rendering or I/O task.
  • Avoid queuing unbounded numbers of microtasks; otherwise, you risk starving macrotasks and degrading performance.

“It’s not about time,” Timothy concluded. “It’s about status.”

“Precisely,” Margaret replied. “In JavaScript, a Promise is a solemn vow. It takes priority over a simple timeout.”

Back to Blog

Related posts

Read more »

From Prop Drilling to Context API😏

Introduction – Why I Decided to Deep‑Dive Into Code Flow For the last few days, I wasn’t just coding — I was trying to understand the soul of a React project....