Check Phase and Close Callbacks
The check phase and close callbacks phase complete the event loop cycle. While less discussed than timers and poll, they serve important roles.
The Check Phase
Purpose
The check phase exists specifically for setImmediate(). It runs callbacks immediately after the poll phase completes.
setImmediate(() => {
console.log('This runs in check phase');
});
Why setImmediate Exists
You might wonder: why not just use setTimeout(fn, 0)?
// These are NOT equivalent:
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
The difference:
setTimeout(fn, 0)→ Runs in timers phase (next iteration)setImmediate(fn)→ Runs in check phase (same iteration, after poll)
The Key Guarantee
Inside an I/O callback, setImmediate always fires first:
const fs = require('fs');
fs.readFile(__filename, () => {
// Currently in poll phase
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
});
// Output: ALWAYS "immediate" then "timeout"
Why? After poll phase finishes, we go to check phase (immediate) BEFORE wrapping back to timers phase (timeout).
Use Cases for setImmediate
1. Break Up CPU-Intensive Work
function processChunk(items, index = 0) {
const chunkSize = 100;
const end = Math.min(index + chunkSize, items.length);
for (let i = index; i < end; i++) {
// Process item
}
if (end < items.length) {
// Yield to event loop, then continue
setImmediate(() => processChunk(items, end));
}
}
2. Execute After Current I/O Callbacks
const http = require('http');
const server = http.createServer((req, res) => {
// This runs in poll phase
setImmediate(() => {
// Runs after ALL current poll phase callbacks
performCleanup();
});
res.end('Hello');
});
3. Avoid Timer Overhead
// setImmediate is faster than setTimeout(fn, 0)
// No timer scheduling overhead
function recursiveProcess(data) {
// Process some data
if (moreData) {
// setImmediate is more efficient for "soon but not now"
setImmediate(() => recursiveProcess(nextChunk));
}
}
Close Callbacks Phase
Purpose
Handles cleanup callbacks when handles are closed abruptly.
const net = require('net');
const socket = net.createConnection({ port: 3000 });
socket.on('close', () => {
console.log('Socket closed - runs in close callbacks phase');
});
// If socket.destroy() is called, 'close' fires in close callbacks phase
socket.destroy();
When Close Callbacks Fire
Close callbacks run when:
- A socket is destroyed with
socket.destroy() - An HTTP connection is abruptly terminated
- A file handle is closed
const http = require('http');
http.createServer((req, res) => {
req.on('close', () => {
// Client disconnected
console.log('Request closed');
});
// Simulate slow response
setTimeout(() => {
res.end('Hello');
}, 10000);
// If client disconnects before 10s, 'close' fires
}).listen(3000);
Close vs End Events
const net = require('net');
const server = net.createServer((socket) => {
socket.on('end', () => {
// Client gracefully closed connection
// Fires in poll phase
console.log('Client ended connection');
});
socket.on('close', () => {
// Connection fully closed (after end, or after error)
// Fires in close callbacks phase
console.log('Socket fully closed');
});
});
setImmediate vs setTimeout(0) Deep Dive
Outside I/O (Non-Deterministic)
// In main script:
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
// Run 1: "timeout", "immediate"
// Run 2: "immediate", "timeout"
// Depends on process startup time!
Why non-deterministic?
- If startup takes > 1ms, timer is already due when loop starts
- If startup takes < 1ms, we hit poll phase first, then check
Inside I/O (Deterministic)
const fs = require('fs');
fs.readFile('file.txt', () => {
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
});
// ALWAYS: "immediate", "timeout"
Performance Comparison
const iterations = 1000;
console.time('setTimeout');
for (let i = 0; i < iterations; i++) {
setTimeout(() => {}, 0);
}
console.timeEnd('setTimeout');
console.time('setImmediate');
for (let i = 0; i < iterations; i++) {
setImmediate(() => {});
}
console.timeEnd('setImmediate');
// setImmediate is typically faster (no timer bookkeeping)
The Complete Event Loop Cycle
const fs = require('fs');
console.log('1. Sync code');
setTimeout(() => console.log('2. Timer'), 0);
setImmediate(() => console.log('3. Immediate'));
fs.readFile(__filename, () => {
console.log('4. I/O callback');
setImmediate(() => console.log('5. Immediate in I/O'));
setTimeout(() => console.log('6. Timer in I/O'), 0);
});
console.log('7. Sync code end');
// Output order:
// 1. Sync code
// 7. Sync code end
// 2. Timer (usually, timing dependent)
// 3. Immediate (usually, timing dependent)
// 4. I/O callback
// 5. Immediate in I/O (ALWAYS before 6)
// 6. Timer in I/O
Key Takeaways
- Check phase runs
setImmediate()callbacks after poll - Close callbacks phase handles socket/handle cleanup
- Inside I/O:
setImmediatealways beatssetTimeout(0) - Outside I/O: Order is non-deterministic
- Use
setImmediateto yield to event loop efficiently