Skip to main content

setTimeout(fn, 0)

setTimeout(fn, 0) does not run the function instantly: it simply puts the callback fn into the macrotask queue so that it runs after the current code and all microtasks have finished. A zero delay means "on the next Event Loop iteration", not "right now".

Theory

TL;DR

  • setTimeout(fn, 0) queues fn as a macrotask instead of running it immediately.
  • The callback waits for the call stack to become free, because the Event Loop never interrupts code that is already running.
  • Every microtask, such as Promise.then and queueMicrotask, runs before it.
  • The order is always: the call stack, then microtasks, then setTimeout.
  • It is the standard way to defer work, to break up a long synchronous block and to let the interface update.
  • The call returns a timer id, which you can cancel with clearTimeout.

Quick example

javascript
console.log('A'); setTimeout(() => console.log('B'), 0); console.log('C');

Output:

text
A C B

Why not A, then B, then C? Because a timer callback is not allowed to cut into code that is already running.

What happens step by step

  1. console.log('A') runs right away, on the call stack.
  2. setTimeout(() => console.log('B'), 0) hands the task to the Web API, as a timer with a 0 ms delay.
  3. The timer "fires" instantly and the callback is added to the macrotask queue.
  4. JavaScript keeps running the current stack, so it prints C.
  5. Once the call stack is empty, the Event Loop takes () => console.log('B') from the queue and runs it.

Why even 0 does not run immediately

Because everything in JavaScript goes through the Event Loop, and it never interrupts the current call stack.

Even with a delay of 0, the callback still has to wait until the stack is free.

So setTimeout(fn, 0) is a way to defer a function to the next Event Loop iteration.

One more detail worth remembering: by specification browsers apply a minimum threshold of about 4 ms for nested timers, so the real delay is almost never zero.

Example with a promise (microtask)

javascript
setTimeout(() => console.log('timeout'), 0); Promise.resolve().then(() => console.log('promise')); console.log('end');

The order:

text
end promise timeout

Explanation:

  • Promise.then() creates a microtask;
  • setTimeout() creates a macrotask;
  • and microtasks always run before macrotasks.

Practical uses

  1. Defer execution so the main code is not blocked:

    javascript
    setTimeout(() => { console.log('Runs after the main code'); }, 0);
  2. Break a synchronous loop so the interface has time to update:

    javascript
    for (let i = 0; i < 1e9; i++) {} // heavy operation console.log('The interface is frozen...'); setTimeout(() => { console.log('Now we run after the pause'); }, 0);
  3. Reset the stack, for example in recursive computations:

    javascript
    function nextStep() { setTimeout(nextStep, 0); // every repetition is a new Event Loop iteration } nextStep();

Summary

PropertyValue
What it doesQueues the callback as a macrotask
When it runsAfter the current stack and all microtasks
A delay of "0"Not instantly, but on the next Event Loop cycle
QueueMacrotasks
ReturnsA timer id, cancellable with clearTimeout

Visually it looks like this:

text
[Call Stack] -> the current code runs here [Microtask Queue] -> promises and queueMicrotask [Task Queue] -> setTimeout(fn, 0)

setTimeout(fn, 0) always waits until:

  1. the call stack is cleared,
  2. the microtasks have run,
  3. and only then does fn run.

Common mistakes

  • Reading 0 as "immediately". It means "as soon as possible once the stack is free", and the difference matters a lot when heavy synchronous code is nearby.
  • Treating setTimeout(fn, 0) and Promise.resolve().then(fn) as equivalent. A promise is a microtask and will always beat the timer.
  • Counting on exactly 0 ms. Browsers clamp nested timers to roughly 4 ms, and in a background tab the delays grow much larger.
  • Curing a frozen interface with a zero timer. It only gives the browser a chance to repaint between chunks of work; if the work itself is heavy, split it into pieces or move it to a Web Worker.
  • Forgetting clearTimeout. A deferred callback can fire after the component has already left the screen and then touch stale data.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.