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:

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:

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?

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

  1. Check phase runs setImmediate() callbacks after poll
  2. Close callbacks phase handles socket/handle cleanup
  3. Inside I/O: setImmediate always beats setTimeout(0)
  4. Outside I/O: Order is non-deterministic
  5. Use setImmediate to yield to event loop efficiently