setImmediate() and the Complete Node.js Event Loop
In Post 3, we met the two new players Node.js adds to the event loop — process.nextTick() and setImmediate(). We went deep on process.nextTick(). Now it's time to complete the picture with setImmediate() — the macrotask that was specifically designed to run right after I/O finishes.
By the end of this post you'll have the complete Node.js event loop in your head — every phase, every player, in the exact order they run.
📝 Note: This post was written with the assistance of AI. The content reflects my personal learning and understanding and should not be taken as professional advice. Please do your own research and due diligence before acting on anything written here.
🌐 What Is I/O?
Before we go further, we need to talk about I/O — because it's central to everything Node.js does.
I/O stands for Input/Output. It just means your program talking to the outside world:
- Reading a file from your computer 📁
- Fetching data from the internet 🌐
- Writing to a database 🗄️
- Sending an email 📧
These operations take time — and crucially, JavaScript doesn't do them itself. It hands them off to the operating system and says "let me know when you're done." Just like the waiter handing an order to the kitchen and walking away.
This is exactly why the event loop exists. Without it, every file read or network request would freeze your entire program while it waited.
⏳ I/O and Promises — Two Separate Moments
Here's something that confuses a lot of people. Promises are microtasks. I/O operations also involve Promises. So where does I/O fit in the event loop?
The answer is — I/O and Promises are two completely separate moments in time.
When you read a file using Node.js:
Step 1 — JavaScript calls fs.readFile()
↓
Step 2 — The actual file reading is handed to the OS
JavaScript moves on immediately — doesn't wait
↓
Step 3 — OS finishes reading the file
This is the I/O phase completing
↓
Step 4 — The result comes back to JavaScript
as a resolved Promise or a callback
↓
Step 5 — The .then() or callback enters the microtask queue
From here, normal microtask rules apply
The I/O operation itself happens outside JavaScript entirely — in the operating system. JavaScript just waits for the signal that it's done. Once that signal arrives, the result re-enters JavaScript as a resolved Promise, and from that point it follows the normal microtask rules we already know.
const fs = require('fs'); // built-in Node.js module for file system operations
console.log("Start");
// fs.readFile hands the file reading to the OS
// the callback only runs after the OS signals it's done
fs.readFile(__filename, () => {
console.log("File is ready"); // runs after I/O completes
});
console.log("End");
Output:
Start
End
File is ready
"Start" and "End" run synchronously. The file reading happens outside JavaScript. Once the OS is done, "File is ready" comes back through the event loop and runs.
The I/O phase is about waiting for the outside world. The microtask queue is about what JavaScript does once it gets the answer back. They're sequential — not competing.
🍽️ What Is setImmediate()?
setImmediate() is a Node.js function that schedules a callback to run after the current I/O phase completes — but before any timers like setTimeout.
The name says it all — run this immediately after whatever I/O just finished. Not after a delay. Not after a timer. Right after the I/O is done.
Think of it like a waiter at a restaurant. The kitchen finishes your pasta and puts it on the pass. The waiter is standing right there — he picks it up and brings it to your table immediately. Meanwhile, a kitchen timer is going off somewhere in the back. Even though the timer says "now", the waiter is already in position and gets there first.
That waiter is setImmediate(). The kitchen timer is setTimeout(..., 0).
const fs = require('fs');
fs.readFile(__filename, () => {
// We are now inside the I/O callback — the file just finished reading
setTimeout(() => {
console.log('Timeout'); // timer — has to wait its turn
}, 0);
setImmediate(() => {
console.log('Immediate'); // already in position — runs first
});
console.log('Inside I/O'); // synchronous — runs right now
});
Output:
Inside I/O
Immediate
Timeout
"Inside I/O" runs first because it's synchronous. Then setImmediate runs before setTimeout — because inside an I/O callback, setImmediate is literally the next phase in the event loop.
setImmediate()was built specifically to run right after I/O finishes. That's its entire purpose — and that's why it beatssetTimeoutin that situation.
🅰️ Inside vs Outside an I/O Callback
Here's the part that trips most people up — setImmediate() doesn't always beat setTimeout. Its behaviour depends on where in your code it's called.
To understand this, think about two different locations in a restaurant:
- The dining room — you're already seated, food is on its way, the waiter knows exactly where to find you
- The parking lot — you haven't even walked in yet, nobody knows you're coming
When you're in the dining room (inside an I/O callback), setImmediate knows exactly where it is in the event loop — right after I/O, in the check phase. It always runs before setTimeout.
When you're in the parking lot (outside an I/O callback, at the top level of your code), neither setImmediate nor setTimeout has a fixed position yet. The order depends on how fast your system processes things at that exact millisecond — and it can change between runs.
// Outside an I/O callback — order is NOT guaranteed
setImmediate(() => {
console.log('Immediate'); // might run first
});
setTimeout(() => {
console.log('Timeout'); // might run first
}, 0);
Output — could be either:
Immediate
Timeout
or
Timeout
Immediate
Don't rely on this order. Outside I/O, it's unpredictable.
const fs = require('fs');
fs.readFile(__filename, () => {
// Inside an I/O callback — order IS guaranteed
setImmediate(() => {
console.log('Immediate'); // always runs first
});
setTimeout(() => {
console.log('Timeout'); // always runs second
}, 0);
});
Output — always:
Immediate
Timeout
The simple rule: Inside an I/O callback —
setImmediatealways beatssetTimeout. Outside — never rely on the order.
🔴 One More Thing — Microtasks Still Go First
Even inside an I/O callback, setImmediate is still a macrotask. And as we established in Post 1 — microtasks always drain completely before any macrotask gets a turn.
This means if you add a Promise or a process.nextTick() inside an I/O callback alongside setImmediate — the microtasks run first, then setImmediate, then setTimeout.
const fs = require('fs');
fs.readFile(__filename, () => {
console.log('Inside I/O'); // synchronous
process.nextTick(() => {
console.log('nextTick'); // VIP microtask
});
Promise.resolve().then(() => {
console.log('Promise'); // regular microtask
});
setImmediate(() => {
console.log('Immediate'); // macrotask — check phase
});
setTimeout(() => {
console.log('Timeout'); // macrotask — timers phase
}, 0);
});
Output:
Inside I/O
nextTick
Promise
Immediate
Timeout
Step by step:
"Inside I/O"— synchronous, runs immediatelyprocess.nextTick(...)— VIP microtask registeredPromise.resolve().then(...)— regular microtask registeredsetImmediate(...)— macrotask registered for check phasesetTimeout(...)— macrotask registered for timers phase- Synchronous code done — microtasks drain:
"nextTick"then"Promise" - Check phase runs:
"Immediate" - Timers phase runs:
"Timeout"
setImmediatealways waits for microtasks to finish first — even when you're already inside an I/O callback. The golden rule from Post 1 never breaks.
🗺️ The Complete Node.js Event Loop
You now have every piece. Here's the full picture — every phase, every player, in the exact order they run:
Synchronous code
↓
process.nextTick() ← VIP microtask, highest priority
↓
Promise .then() / await ← regular microtask
↓
I/O callbacks ← file reads, network requests land here
↓
[microtasks drain again if added inside I/O callback]
↓
setImmediate() ← check phase, right after I/O
↓
setTimeout / setInterval ← timers phase
↓
[repeat forever]
Every async operation in Node.js — every Promise, every file read, every timer, every setImmediate — flows through this system in this exact order.
📋 The Complete Player Reference
Here's every player we've covered across this series, in one place:
| Player | Type | Environment | When It Runs |
|---|---|---|---|
| Synchronous code | — | Both | Immediately, right now |
process.nextTick() | VIP microtask | Node.js only | After current operation, before everything |
Promise .then() / await | Microtask | Both | After sync code, before macrotasks |
| I/O callbacks | Macrotask | Node.js | After microtasks, when OS signals done |
setImmediate() | Macrotask | Node.js only | Check phase, right after I/O |
setTimeout() / setInterval() | Macrotask | Both | Timers phase, after check phase |
| UI events | Macrotask | Browser only | When user interacts |
✅ Key Takeaways
setImmediate()is a Node.js macrotask designed to run right after I/O completes- Inside an I/O callback —
setImmediatealways runs beforesetTimeout— guaranteed - Outside an I/O callback — the order between
setImmediateandsetTimeoutis unpredictable — never rely on it - Microtasks still go first — even inside I/O,
process.nextTick()and Promises drain beforesetImmediategets a turn - The complete Node.js order is: sync →
nextTick→ Promises → I/O →setImmediate→setTimeout
You now have the complete Node.js event loop in your head. Every player, every phase, every rule — from synchronous code all the way down to timers. Nothing runs by accident.
🔮 What's Next
In Post 5, we put everything to the test — a series of progressively harder code puzzles covering both browser and Node.js scenarios. No explanations upfront — just code, your prediction, and a full breakdown after.
This is Part 4 of a 5-part series on the JavaScript Event Loop.

