# Why Does setImmediate Sometimes Run Before setTimeout(fn, 0)?

> **Author:** Saurabh Prajapati · Software Engineer @ IBM India · May 2025

* * *

## The Mystery That Started This

I was writing some async Node.js code and stumbled across something that genuinely confused me. Look at this snippet:

```js
setTimeout(() => console.log("setTimeout"), 0);
setImmediate(() => console.log("setImmediate"));
```

I ran it and got:

```plaintext
setImmediate
setTimeout
```

Wait… what? I set the timeout to `0` milliseconds. That means "run immediately," right? So why did `setImmediate` run first?

Then I added one line — just a `console.log` at the top — and everything flipped:

```js
console.log("Hello from console");
setTimeout(() => console.log("setTimeout"), 0);
setImmediate(() => console.log("setImmediate"));
```

Output:

```plaintext
Hello from console
setTimeout
setImmediate
```

Now `setTimeout` runs before `setImmediate`. How?! Both snippets look almost identical. This drove me crazy until I finally understood what Node.js is *actually* doing under the hood.

Let me walk you through exactly what I learned.

* * *

## First, Let's Understand the Node.js Event Loop

Before we can solve this mystery, we need a mental model of how Node.js handles async code. Node.js is single-threaded, which means it can only do one thing at a time. But it can *schedule* things to happen later using the **event loop**.

Think of the event loop like a to-do list manager. It has different trays for different kinds of tasks, and it processes each tray in a specific order — every single tick.

The simplified order of phases looks like this:

```plaintext
┌─────────────────────────────────┐
│         timers phase            │  ← runs setTimeout / setInterval callbacks
├─────────────────────────────────┤
│      pending callbacks          │
├─────────────────────────────────┤
│        idle / prepare           │
├─────────────────────────────────┤
│          poll phase             │  ← waits for I/O, fetches new events
├─────────────────────────────────┤
│         check phase             │  ← runs setImmediate callbacks  ✅
├─────────────────────────────────┤
│      close callbacks            │
└─────────────────────────────────┘
         ↑ loops back up ↑
```

Two phases matter most for our mystery:

**Timers phase** is where `setTimeout` and `setInterval` callbacks live. Node checks: "Has enough time passed for this timer to be due?" If yes, it runs the callback.

**Check phase** is where `setImmediate` callbacks live. This phase always runs *after* the poll phase, every single loop iteration.

So the order in the loop is always: **timers → ... → poll → check**. That means `setTimeout` callbacks are *checked before* `setImmediate` callbacks in the phase order. You'd expect `setTimeout` to always win, right?

This is where it gets interesting.

* * *

## The Real Culprit: Timer Precision

Here's the thing about `setTimeout(fn, 0)` — it doesn't actually mean "zero milliseconds." It means "run this after *at least* 0 milliseconds." The minimum delay in Node.js is technically `1ms`, and even that isn't guaranteed because reading the system clock takes a tiny amount of time.

So when Node.js starts the event loop and reaches the **timers phase**, it asks the system clock: "Is the timer ready?" If that clock read happens to be just a hair too early — say the timer was registered `0.3ms` ago and the minimum is `1ms` — Node says "not yet" and **skips** the `setTimeout` callback for this tick.

Then the event loop continues to the **check phase**, runs `setImmediate`, and only on the *next* tick does it come back to timers and find that `setTimeout` is now ready.

This is why the output of the first snippet is non-deterministic — you might even see it flip if you run it multiple times. It entirely depends on the timing of when Node reads the clock.

* * *

## So Why Does Code 2 Always Produce a Consistent Order?

This is the key insight. When you add `console.log("Hello from console")` at the top, you're running that code **synchronously** — it executes before the event loop even starts. The `setTimeout` and `setImmediate` are registered *after* that synchronous work completes.

By the time Node.js finishes the synchronous code and enters the event loop, a small but real amount of time has already passed. That extra time is almost always enough for the `1ms` minimum timer threshold to be comfortably satisfied.

So when the event loop reaches the timers phase, it checks: "Is the `setTimeout(0)` ready?" And now the answer is **yes** — because enough real time has passed. It runs the `setTimeout` callback first, then moves on to the check phase and runs `setImmediate`.

Think of it this way:

> **No sync work before them** → timer might not be ready → `setImmediate` can sneak in first → *order is unpredictable.*
> 
> **Sync work before them** → timer is definitely ready by the time event loop starts → `setTimeout` runs first → *order is predictable.*

* * *

## Let's Cement This With the Timeline

Here's exactly what happens in **Code 1** (no prior sync work):

```plaintext
1. Node.js starts the event loop almost immediately
2. Enters TIMERS phase → checks setTimeout(0)
   → "Is 1ms elapsed?" → Maybe not yet! → SKIPPED
3. Enters CHECK phase → runs setImmediate  ← prints "setImmediate"
4. Next iteration → TIMERS phase again
   → "Is 1ms elapsed?" → YES now → runs setTimeout  ← prints "setTimeout"
```

And here's what happens in **Code 2** (with a sync `console.log` first):

```plaintext
1. Synchronous code runs: console.log("Hello from console")  ← prints immediately
   (This takes a few microseconds of real time)
2. Node.js enters the event loop
3. Enters TIMERS phase → checks setTimeout(0)
   → "Is 1ms elapsed?" → YES, sync work already used that time → runs it  ← prints "setTimeout"
4. Enters CHECK phase → runs setImmediate  ← prints "setImmediate"
```

* * *

## One More Thing

If you call `setTimeout` and `setImmediate` *inside an I/O callback*, the order becomes **always predictable** — `setImmediate` will *always* run before `setTimeout`, no matter what. That's because I/O callbacks fire during the poll phase, and the check phase comes *right after* poll in every single loop iteration.

```js
const fs = require("fs");

fs.readFile(__filename, () => {
  setTimeout(() => console.log("setTimeout"), 0);
  setImmediate(() => console.log("setImmediate"));
});
```

Output is *always*:

```plaintext
setImmediate
setTimeout
```

Inside an I/O callback, you're already past the timers phase. So `setTimeout` cannot possibly run until the *next* loop iteration. But `setImmediate` runs in the very next phase (check), so it always wins here.

This is why `setImmediate` is the recommended way to schedule something "right after the current I/O cycle" — it's predictable and reliable inside async callbacks.

* * *

## The Final Answer

Let's bring it all together. The two snippets behave differently because:

**Code 1** gives Node.js almost no time before the event loop starts. The `setTimeout(0)` timer might not have crossed the `1ms` minimum threshold by the time the timers phase runs, so `setImmediate` (which lives in the check phase, guaranteed to run every loop) gets a chance to run first. This makes the output **non-deterministic**.

**Code 2** runs a synchronous `console.log` first. That tiny bit of real-world execution time is enough for the timer to be considered ready by the time the event loop's timers phase runs. So `setTimeout` reliably fires first, followed by `setImmediate`. The output is **predictable**.

The deeper lesson is this: `setTimeout(fn, 0)` does **not** mean "run right now." It means "run as soon as possible after at least 1ms." The event loop's timer phase simply checks whether the delay has been satisfied — and sometimes the answer is "not quite yet."

* * *

## Quick Reference

|  | `setTimeout(fn, 0)` | `setImmediate(fn)` |
| --- | --- | --- |
| **Phase** | Timers | Check |
| **Delay guarantee** | At least ~1ms | After current poll phase |
| **Order vs each other** | Non-deterministic at top level | Non-deterministic at top level |
| **Inside I/O callback** | Runs next iteration | Always runs first |
| **Use when** | Delaying by time | Scheduling after I/O |

* * *

## Try It Yourself

Run both snippets a few times and watch the output of Code 1 vary between runs — you might see it flip. Then wrap them inside a `fs.readFile` callback and notice that `setImmediate` always wins. Playing with it hands-on is what made this truly click for me.

* * *

## About the Author

**Saurabh Prajapati** is a Full-Stack Software Engineer at IBM India Software Lab, working on Maximo — cloud-native enterprise solutions. He specializes in GenAI, React, and modern web technologies, and loves documenting his learning journey so others don't have to stumble on the same confusing bugs.

Connect with him: [GitHub — prajapatisaurabh](https://github.com/prajapatisaurabh) · [LinkedIn — saurabh-prajapati](https://linkedin.com/in/saurabh-prajapati) · saurabhprajapati120@gmail.com
