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.
The output is:
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:
The output is:
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
- '
setTimeoutcallbacks 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:
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
0argument is a minimum timer request, not a strict "run first" instruction.

