Timers & the Event Loop Phases

Reviewed & published by Brayan K

Node's timers (setTimeout, setInterval, setImmediate) and process.nextTick are scheduling tools that hand callbacks to specific phases of the event loop, which decides the exact order your asynchronous code runs in.

Part of the free Node.js course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

By the end of this lesson you'll be able to schedule and cancel timers, predict the precise order of nextTick, promise microtasks, timers, and setImmediate, and explain the event-loop phases that make Node fast and single-threaded.

What You'll Learn in This Lesson

1️⃣ The Timer Functions

Node gives you four scheduling globals that work without any require. setTimeout(fn, ms) runs a callback once after a delay and returns a handle; pass that handle to clearTimeout to cancel it before it fires. setInterval(fn, ms) repeats forever until you stop it with clearInterval.

The delay is a minimum, not a guarantee: Node will run your callback as soon as it can after that time, once the event loop reaches the timers phase. That's why the 30ms interval below ticks once before the 50ms timeout, then continues.

// Timers are global in Node — no require needed.
// setTimeout schedules a callback to run ONCE, after a delay (in ms).
const handle = setTimeout(() => console.log('never runs'), 1000);

// clearTimeout cancels a pending setTimeout before it fires.
clearTimeout(handle);

// This one is NOT cancelled, so it fires after ~50ms.
setTimeout(() => console.log('fires after ~50ms'), 50);

// setInterval repeats forever until you stop it with clearInterval.
let count = 0;
const id = setInterval(() => {
  count++;
  console.log(`tick ${count}`);
  if (count === 3) clearInterval(id);   // stop after 3 ticks
}, 30);

2️⃣ The Order Everything Runs In

This is the part that trips up even experienced developers. After your synchronous code finishes, Node drains queues in a strict priority order: first the process.nextTick queue, then the promise microtask queue, and only then does the event loop advance to phases like timers (setTimeout) and check (setImmediate).

The example below schedules one of each, deliberately out of order, then numbers the output so you can see the true sequence. Run it in a .cjs (CommonJS) file to see this canonical ordering.

// Run this with: node order.cjs
console.log('1: synchronous start');

// These two jump the queue: nextTick and microtasks run as soon as the
// current operation finishes, before the loop moves on at all.
process.nextTick(() => console.log('3: process.nextTick'));
Promise.resolve().then(() => console.log('4: promise .then (microtask)'));

console.log('2: synchronous end');

// setTimeout(fn, 0) versus setImmediate is the ONE pair whose order is not
// fixed when you schedule them from the main module. It depends on how long
// the process took to start, so the same file genuinely prints them either
// way on different runs — measured here over eight runs, four each way.
// Any tutorial that promises you an order for those two from the top level
// is describing one run of one machine.
//
// Inside an I/O callback the answer IS defined, and this is why: the poll
// phase (where I/O callbacks run) is immediately followed by the check
// phase (where setImmediate runs), while timers only come round on the next
// turn of the loop. So schedule them there and the order is guaranteed:
require('node:fs').readFile(__filename, () => {
  setTimeout(() => console.log('6: setTimeout(fn, 0) — timers phase, next turn'), 0);
  setImmediate(() => console.log('5: setImmediate — check phase, this turn'));
});

3️⃣ nextTick Always Beats Timers

The process.nextTick queue has the highest priority of all. Every nextTick callback registered during the current operation runs — to completion — before the event loop is allowed to move to the timers phase, even for a timer that was registered before the nextTick calls.

That power is a double-edged sword: if a nextTick callback schedules another nextTick, which schedules another, you can starve the loop and stop timers and I/O from ever running. Use it sparingly and intentionally.

// All process.nextTick callbacks drain BEFORE the event loop
// moves on to any timer — even ones registered earlier.
process.nextTick(() => console.log('A: nextTick before any timer'));
setTimeout(() => console.log('C: timer'), 0);
process.nextTick(() => console.log('B: second nextTick, still before timer'));

Your turn. The program below works once you fill in the three blanks marked ___. Follow the 👉 hints, then run it and compare with the expected output — the order is the whole point.

// Run with: node turn.cjs
console.log('start');

// 🎯 YOUR TURN — fill each ___ with the right scheduler.

// 1) Schedule this to run in the NEXT tick, before promises:
___(() => console.log('nextTick'));        // 👉 process.nextTick

// 2) Schedule this microtask with a resolved promise:
Promise.___().then(() => console.log('promise'));   // 👉 resolve

// 3) Schedule this for the timers phase with zero delay:
___(() => console.log('timeout'), 0);      // 👉 setTimeout

console.log('end');

// ✅ Expected output (order matters!):
//    start
//    end
//    nextTick
//    promise
//    timeout

Mini-Challenge: A Countdown Timer

No blanks this time — just a brief and an outline. Write it yourself, run it, and check your output against the example in the comments. This ties together setInterval and clearInterval in one short program.

// 🎯 MINI-CHALLENGE: a countdown timer
// Print 3, then 2, then 1 (one per ~100ms using setInterval),
// then print "Liftoff!" and STOP the interval with clearInterval.
//
// Outline:
//   let n = 3;
//   const id = setInterval(() => {
//     if (n === 0) { /* print Liftoff!, clearInterval(id), return */ }
//     // else print n and decrement
//   }, 100);
//
// ✅ Example output:
//    3
//    2
//    1
//    Liftoff!

// your code here

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — Scheduling & Order

SchedulerRuns
process.nextTick(fn)before microtasks & every phase
Promise.then(fn)microtask, after nextTick
setTimeout(fn, 0)timers phase
setInterval(fn, ms)timers phase, repeatedly
setImmediate(fn)check phase

🎉 Lesson Complete!

Practice quiz

What does setTimeout(fn, ms) do?

  • Runs fn repeatedly every ms
  • Runs fn once after at least ms milliseconds
  • Runs fn synchronously now
  • Cancels a timer

Answer: Runs fn once after at least ms milliseconds. setTimeout schedules fn to run once after the delay (a minimum, not exact).

How do you cancel a pending setTimeout?

  • clearTimeout(handle)
  • stopTimeout(handle)
  • setTimeout(null)
  • delete handle

Answer: clearTimeout(handle). Pass the handle setTimeout returned to clearTimeout to cancel it before it fires.

After synchronous code, which queue drains FIRST?

  • setTimeout timers
  • setImmediate
  • process.nextTick
  • I/O callbacks

Answer: process.nextTick. The nextTick queue has the highest priority — it drains before microtasks and every phase.

In Node, which runs LAST among these in one turn?

  • process.nextTick
  • promise .then microtask
  • synchronous code
  • setImmediate

Answer: setImmediate. Order is sync → nextTick → promise microtasks → timers → setImmediate (check phase).

Why doesn't setTimeout(fn, 0) run immediately?

  • It is a macrotask that waits for the timers phase
  • It is cancelled automatically
  • Zero means never
  • It needs a require

Answer: It is a macrotask that waits for the timers phase. It runs after all sync code, nextTicks, and microtasks — it is a macrotask.

How do you stop a repeating setInterval?

  • clearTimeout(id)
  • clearInterval(id)
  • return false
  • setInterval(0)

Answer: clearInterval(id). Store the id setInterval returns and pass it to clearInterval.

Which is a microtask in Node?

  • setImmediate
  • setInterval
  • A resolved promise's .then callback
  • An fs read callback

Answer: A resolved promise's .then callback. Promise .then/.catch/.finally callbacks are microtasks; timers/immediate are macrotasks.

What risk comes from recursive process.nextTick calls?

  • A SyntaxError
  • The timer fires twice
  • Faster I/O
  • They can starve the event loop so timers and I/O never run

Answer: They can starve the event loop so timers and I/O never run. nextTick scheduling more nextTicks blocks the loop from advancing.

Which phase runs setImmediate callbacks?

  • The check phase
  • The timers phase
  • The poll phase
  • The close phase

Answer: The check phase. setImmediate callbacks run in the dedicated check phase, after poll.

Do the timer functions need a require in Node?

  • Yes, require('timers')
  • Only setInterval does
  • No — they are global
  • Only in ES modules

Answer: No — they are global. setTimeout, setInterval, setImmediate, and clearTimeout are global in Node.

Continue this course

Frequently asked questions

What is the difference between process.nextTick and setImmediate?

They sound similar but fire at opposite ends of the loop. process.nextTick callbacks run immediately after the current operation finishes, before the event loop continues to any phase — so they jump ahead of timers, I/O, and even promise microtasks in some cases. setImmediate runs in the dedicated 'check' phase, after the current poll phase completes. Rule of thumb: process.nextTick is 'right now, before anything else'; setImmediate is 'on the next pass of the loop'. Overusing nextTick can starve the loop, so reach for setImmediate when you just want to defer work.

Why does setTimeout(fn, 0) not run instantly?

setTimeout(fn, 0) means 'run as soon as possible', not 'run synchronously'. The callback is placed in the timers phase of the event loop, which only runs after all synchronous code, all process.nextTick callbacks, and all pending promise microtasks have finished. Node also enforces a minimum delay (historically about 1ms), so a 0 is effectively a 'very soon, but not now' schedule.

What are microtasks versus macrotasks in Node?

Microtasks are the high-priority queue: resolved/rejected promise callbacks (.then/.catch/.finally) and queueMicrotask. They drain completely between each macrotask. Macrotasks are the larger units of work scheduled by timers (setTimeout/setInterval) and setImmediate, plus I/O callbacks. The pattern each turn is: run a macrotask, then empty the entire microtask queue, then move on. process.nextTick sits in its own even-higher-priority queue that drains before microtasks.

What are the phases of the Node event loop?

Each iteration of the loop moves through fixed phases in order: timers (setTimeout/setInterval callbacks whose time is up), pending callbacks (some deferred system callbacks), poll (retrieve new I/O events and run their callbacks), check (setImmediate callbacks), and close callbacks (like 'close' events on sockets). Between phases, Node drains the process.nextTick queue and then the promise microtask queue. Understanding this order explains why your callbacks fire when they do.