Almost every JavaScript developer has seen this:
console.log("Start");
setTimeout(() => {
console.log("setTimeout");
}, 0);
Promise.resolve().then(() => {
console.log("Promise");
});
console.log("End");
Output
Start
End
Promise
setTimeout
At first glance, this seems strange.
setTimeout(..., 0) looks like it should execute immediately. So why does the Promise callback run first?
The answer lies in JavaScript's Event Loop.
👉 Run this in JS Visualizer — watch the queues in real time
Step 1: JavaScript Executes Synchronous Code First
Everything in the current script executes on the Call Stack.
console.log("Start");
prints immediately.
When JavaScript encounters:
setTimeout(() => {
console.log("setTimeout");
}, 0);
it does not execute the callback.
Instead, it hands the callback to the browser (or Node.js) timer system and continues executing the current script.
Next:
Promise.resolve().then(() => {
console.log("Promise");
});
The Promise is already resolved, so its .then() callback is immediately scheduled in the Microtask Queue.
Finally:
console.log("End");
prints.
Current output:
Start
End
Step 2: The Call Stack Becomes Empty
At this point:
Microtask Queue
---------------
Promise callback
Task Queue (Macrotask Queue)
----------------------------
setTimeout callback
Step 3: The Event Loop Rule
The Event Loop always follows this order:
- Execute one Task (current script).
- Empty the entire Microtask Queue.
- Optionally allow rendering.
- Execute the next Task (Macrotask).
Because Promise callbacks are Microtasks, they execute before timer callbacks.
Output now becomes:
Start
End
Promise
Step 4: Finally, setTimeout Runs
After the Microtask Queue is empty, the Event Loop processes the next Task:
setTimeout
Final output:
Start
End
Promise
setTimeout
Common Misconception
❌ Promises have higher priority than setTimeout.
This is an oversimplification.
✅ The real reason is:
The Event Loop must completely drain the Microtask Queue before executing the next Task (Macrotask).
Why Doesn't setTimeout(0) Run Immediately?
setTimeout(fn, 0) means:
Execute after at least 0 milliseconds.
It does not mean "execute instantly."
Even after the timer expires, its callback waits until:
- The current script finishes.
- The Call Stack becomes empty.
- Every pending Microtask has completed.
Visual Timeline
Current Script
│
├── console.log("Start")
├── setTimeout(...) ─────► Browser Timer
├── Promise.then() ───────► Microtask Queue
└── console.log("End")
Call Stack Empty
│
▼
Drain ALL Microtasks
│
▼
Promise callback
▼
Run Next Task
│
▼
setTimeout callback
Example 2
console.log(1);
setTimeout(() => console.log(2), 0);
Promise.resolve()
.then(() => console.log(3))
.then(() => console.log(4));
console.log(5);
Output
1
5
3
4
2
Why?
Synchronous → 1, 5
Microtasks → 3, 4
Tasks → 2
The second .then() is also a Microtask, so it executes before setTimeout.
👉 Run Example 2 in JS Visualizer
Example 3
console.log("Start");
setTimeout(() => {
console.log("Timeout");
}, 0);
Promise.resolve().then(() => {
console.log("Promise 1");
Promise.resolve().then(() => {
console.log("Promise 2");
});
});
console.log("End");
Output
Start
End
Promise 1
Promise 2
Timeout
While executing Promise 1, another Microtask is scheduled. The Event Loop continues draining Microtasks until none remain.
👉 Run Example 3 in JS Visualizer
Microtask Starvation
function loop() {
Promise.resolve().then(loop);
}
loop();
Because every Microtask schedules another Microtask:
-
setTimeoutnever executes. - Rendering may be blocked.
- The application appears frozen.
This is called Microtask Starvation.
👉 Run Microtask Starvation in JS Visualizer
Note: The starvation example uses a counter limit so it doesn't actually freeze your browser — but you'll see all microtasks drain before the setTimeout fires.
Mental Model
Current Script
↓
Drain ALL Microtasks
↓
Browser may Render
↓
Run ONE Task
↓
Repeat
Key Takeaways
-
setTimeout(..., 0)schedules a Task (Macrotask) after at least the specified delay. - Promise callbacks (
then,catch,finally) are Microtasks. - The Event Loop always drains the entire Microtask Queue before processing the next Task.
- This is why Promises execute before
setTimeout(). - Microtasks can enqueue more Microtasks, and they all finish before the next Task.
Want to see all of this animate live? Open JS Visualizer — free, no signup and step through any example above.
Related reads:
Top comments (0)