Timers

setInterval & The Drift Problem

setInterval is used to run a function repeatedly at a set interval.

setInterval(() => console.log("Tick"), 1000);

The Flaw: setInterval does not care if our function takes time to run. It schedules the next execution based on when the last one was scheduled, not when it finished.

  • Scenario: We want a task every 1000ms.
  • Reality: If the task takes 400ms to run, we only have 600ms of rest before the next one starts.
  • The Drift: If the main thread is blocked, intervals can bunch up and fire inefficiently back-to-back.

The Fix: Recursive setTimeout. This ensures the next timer only starts after the current one finishes.

function tick() {
  console.log("Tick");
  // Only schedule the next one when this one is done
  setTimeout(tick, 1000);
}
setTimeout(tick, 1000);

setTimeout(fn, 0)

Many developers use setTimeout(fn, 0) to make code "async."

Does 0 mean Immediate? No. It means The Minimum Possible Delay.

  1. Clamping: The Environment enforces a minimum delay. It is never truly 0ms.
  2. Macrotask: It pushes the callback to the Macrotask Queue. The engine must still finish the current script, clear the Call Stack, and flush the Microtask Queue (Promises) before it touches this timer.

setImmediate

Note: setImmediate is not a standard Web API (browsers don't support it). It is specific to the Node.js Event Loop.

Purpose: To execute a script immediately after the current Poll Phase (I/O) completes.

While setTimeout technically belongs to the Timers phase, setImmediate belongs to the Check phase of the Node.js Event Loop.


The Showdown: setTimeout vs setImmediate

This is the critical architectural distinction in Node.js.

Scenario A: The Main Module

If we run them in the main script, the order is Non-Deterministic. It depends on system performance.

// index.js
setTimeout(() => console.log("Timeout"), 0);
setImmediate(() => console.log("Immediate"));
 
// Output: Could be "Timeout" -> "Immediate" OR "Immediate" -> "Timeout"

Scenario B: Inside I/O

If we are inside an I/O callback (like reading a file), setImmediate is guaranteed to run first.

const fs = require("fs");
 
fs.readFile(__filename, () => {
  // We are now in the I/O Phase
  setTimeout(() => console.log("Timeout"), 0);
  setImmediate(() => console.log("Immediate"));
});

Why?

  1. Node is in the I/O Phase.
  2. The next phase in the loop is Check (where setImmediate lives).
  3. The phase after that is Close, then it loops back to Timers (where setTimeout lives).

Result: setImmediate runs in the current tick. setTimeout must wait for the next tick.

Output (Always):

Immediate
Timeout

6. Summary Table

TimerEnvironmentPhase/QueueExecution Time
setTimeout(fn, 0)Browser & NodeMacrotask (Timers)Next Loop (min 1-4ms delay).
setImmediateNode.js onlyCheck PhaseImmediately after I/O.
setIntervalBrowser & NodeMacrotask (Timers)Repeats (Subject to drift).

Stop and Think: Why does setTimeout(fn, 0) help render the UI ?

Answer: The UI rendering happens in between Macrotasks. By using setTimeout, we yield control back to the Event Loop. The browser says, Stack is empty? Okay, let me repaint the screen first, then I'll pick up your timeout callback. This prevents the Frozen Screen issue during heavy calculations.