DEV Community

Cover image for Why Do Promises Run Before setTimeout? (Explained Visually)
Supratik Das
Supratik Das

Posted on • Originally published at jsvisualizer.bytefront.dev

Why Do Promises Run Before setTimeout? (Explained Visually)

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'));
Enter fullscreen mode Exit fullscreen mode

Output:

promise
setTimeout
Enter fullscreen mode Exit fullscreen mode

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:

  1. All currently-running synchronous code to finish
  2. 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'));
Enter fullscreen mode Exit fullscreen mode

Here's the sequence:

  1. setTimeout hands its callback to the Web API timer (0ms)
  2. Promise.resolve().then() queues its callback as a microtask (Promise is already resolved, so it's instant)
  3. Synchronous code ends → call stack is empty
  4. Event loop checks: any microtasks? Yes → runs promise
  5. 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');
Enter fullscreen mode Exit fullscreen mode

Output:

script start
script end
promise 1
promise 2
setTimeout
Enter fullscreen mode Exit fullscreen mode

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.

👉 Run this in JS Visualizer


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
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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.then callbacks are microtasks — highest priority async work
  • setTimeout callbacks 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/await follows the same microtask priority

Go deeper


What's the most surprising async ordering you've encountered? Drop it in the comments.

Top comments (0)