JavaScript
setTimeout
asynchronous
event loop
programming

Why is the setTimeout with 0 seconds last to finish

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

setTimeout(fn, 0) does not mean "run immediately." It means "place this callback in the timer task queue as soon as the minimum timer delay has expired, then run it only after the current stack and higher-priority queued work have finished."

The current call stack always runs first

JavaScript executes one stack of synchronous work at a time. If you schedule a timer during that work, the callback cannot interrupt the code that is already running.

javascript
1console.log("A");
2
3setTimeout(() => {
4  console.log("timeout");
5}, 0);
6
7console.log("B");

The output is:

text
A
B
timeout

That happens because the timer callback is queued for later. The current synchronous code must finish first.

Zero milliseconds is a minimum delay, not a promise

The 0 in setTimeout(fn, 0) means the timer becomes eligible as soon as possible. It does not mean the callback jumps ahead of the event loop.

The browser or runtime still has to:

  • finish the current call stack
  • process microtasks
  • pick the next macrotask from the queue

Only then can the timeout callback run.

Microtasks run before timeout callbacks

Promises are the common source of confusion here because they use the microtask queue, which runs before the next timer task:

javascript
1console.log("start");
2
3setTimeout(() => {
4  console.log("timeout");
5}, 0);
6
7Promise.resolve().then(() => {
8  console.log("promise");
9});
10
11console.log("end");

The output is:

text
1start
2end
3promise
4timeout

So even after the synchronous code ends, the timeout still waits behind queued microtasks.

setTimeout callbacks are macrotasks

In common event-loop terminology:

  • promise callbacks are microtasks
  • 'setTimeout callbacks are macrotasks or task-queue jobs'

The event loop drains microtasks before moving on to the next timer or other queued task. That is a big reason setTimeout(fn, 0) often appears to finish "last."

Long synchronous work delays everything

If the main thread is busy, the timeout callback waits even longer:

javascript
1setTimeout(() => {
2  console.log("timer fired");
3}, 0);
4
5const start = Date.now();
6while (Date.now() - start < 200) {
7  // busy loop
8}
9
10console.log("loop done");

The timer cannot run during the blocking loop. The event loop only gets a chance to process it after the synchronous work ends.

Browsers may also clamp nested timers

Browsers sometimes impose a minimum timeout delay greater than zero, especially for nested timers or background tabs. That means even the "eligible as soon as possible" part may be stretched by runtime rules.

So the timer delay is influenced by both:

  • event-loop ordering
  • environment-specific timer clamping

The right mental model

Do not think of setTimeout(..., 0) as "run now." Think of it as:

  • "run this later, after the current work and after microtasks, when the event loop gets to timer tasks"

That mental model explains most timing surprises.

Common Pitfalls

  • Expecting setTimeout(fn, 0) to interrupt synchronous code immediately.
  • Forgetting that promise callbacks run before timeout callbacks.
  • Assuming the numeric delay is an exact execution timestamp.
  • Ignoring long-running synchronous work that blocks the event loop.
  • Treating timers as a scheduling guarantee rather than a queued request.

Summary

  • 'setTimeout(fn, 0) queues a callback for later; it does not run it immediately.'
  • The current call stack must finish before the timeout can run.
  • Microtasks such as promise callbacks run before the next timeout callback.
  • Long synchronous work can delay timers far beyond the requested delay.
  • The 0 argument is a minimum timer request, not a strict "run first" instruction.

Course illustration
Course illustration

All Rights Reserved.