It's one of the most common JavaScript interview questions, and one of the most confusing things to run into for the first time.
You register a setTimeout(fn, 0) before a Promise.then() — yet the Promise callback runs first. Why?
setTimeout(() => console.log('setTimeout'), 0);
Promise.resolve().then(() => console.log('promise'));
Output:
promise
setTimeout
If that surprised you, you're in exactly the right place.
The one-sentence answer
Promise callbacks are microtasks, and the event loop drains the entire microtask queue before it runs the next macrotask — and setTimeout callbacks are macrotasks.
So even a 0ms timer has to wait until every pending Promise callback has finished.
Microtask vs Macrotask — what goes where?
| Type | Examples |
|---|---|
| Microtask |
Promise.then/catch/finally, queueMicrotask(), await continuations |
| Macrotask |
setTimeout, setInterval, I/O events, UI events |
The event loop's rule is absolute: after every macrotask (including the initial script), drain the entire microtask queue before picking up the next macrotask.
What setTimeout(fn, 0) actually means
The 0 is not "run now." It's a minimum delay.
When that timer elapses, the Web API moves the callback into the macrotask queue — where it waits for:
- All currently-running synchronous code to finish
- The entire microtask queue to drain
Only then does a macrotask run.
Step by step — exactly what happens
setTimeout(() => console.log('setTimeout'), 0);
Promise.resolve().then(() => console.log('promise'));
Here's the sequence:
-
setTimeouthands its callback to the Web API timer (0ms) -
Promise.resolve().then()queues its callback as a microtask (Promise is already resolved, so it's instant) - Synchronous code ends → call stack is empty
- Event loop checks: any microtasks? Yes → runs
promise✅ - Microtask queue empty → picks up next macrotask → runs
setTimeout✅
See it happen live
The easiest way to internalize this is to watch it rather than reason about it.
👉 Open this example in JS Visualizer
JS Visualizer is a free, browser-based tool that shows the call stack, Web APIs, task queue, and microtask queue as your code runs — step by step.
Hit Step and watch:
- The Promise callback appear in the Microtask Queue
- The setTimeout callback sit in Web APIs, then move to the Task Queue
- The microtask drain first, before the task queue is touched
The ordering becomes completely obvious when you can see both queues side by side.
A slightly more complex example
console.log('script start');
setTimeout(() => console.log('setTimeout'), 0);
Promise.resolve()
.then(() => console.log('promise 1'))
.then(() => console.log('promise 2'));
console.log('script end');
Output:
script start
script end
promise 1
promise 2
setTimeout
Both Promise callbacks finish — including the one chained from the first .then() — before setTimeout gets a turn. The microtask queue drains completely, including any microtasks added during the draining pass.
The practical implication
This means a flood of Promise callbacks can delay your timers. If you have code like:
// Don't rely on this running "immediately"
setTimeout(updateUI, 0);
// If something upstream kicks off a long Promise chain...
await doLotsOfWork(); // each .then adds another microtask
The updateUI timer won't fire until the entire Promise chain resolves. In practice this usually isn't a problem — but it's why setTimeout(fn, 0) is not a precise scheduling tool.
One more: async/await is microtasks too
async function run() {
console.log('A');
await null; // everything after this is a microtask
console.log('B');
}
setTimeout(() => console.log('timeout'), 0);
run();
console.log('C');
Output: A → C → B → timeout
await suspends the function and schedules the rest as a microtask — same priority as Promise.then. So B beats timeout for the same reason.
👉 Watch the await suspension in JS Visualizer
Summary
-
Promise.thencallbacks are microtasks — highest priority async work -
setTimeoutcallbacks are macrotasks — run one per event loop tick - The event loop always drains the entire microtask queue before touching the task queue
-
setTimeout(fn, 0)means "as soon as possible after microtasks" — not "immediately" -
async/awaitfollows the same microtask priority
Go deeper
- JavaScript Event Loop Explained — A Visual, Step-by-Step Guide
- Microtask vs Macrotask in JavaScript: The Complete Guide
What's the most surprising async ordering you've encountered? Drop it in the comments.
Top comments (0)