Timers & the Event Loop Phases
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.
Learn Timers & the Event Loop Phases in our free Node.js course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a…
Part of the free Node.js course at LearnCodingFast — hands-on lessons with examples you run in your browser, 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.
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.
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.
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.
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.
Common Errors (and the fix)
- ❌ Your program never exits — A setInterval is still running. ✅ Store its id and call clearInterval(id) when you're done (or use unref() on the handle).
- ❌ Expected setTimeout(fn, 0) to run "now" — It can't; it's a macrotask. ✅ All synchronous code, nextTicks, and promise microtasks run first. Use process.nextTick if you truly need "before the next phase".
- ❌ Recursive process.nextTick froze my app — A nextTick that schedules another nextTick starves the loop. ✅ Use setImmediate to defer instead, so I/O and timers still get a turn.
- ❌ A wrong this inside the callback — Passing a method reference loses its binding. ✅ Use an arrow function: setTimeout(() => obj.run(), 100) .
- ❌ Off-by-one delays — Timers fire after their delay, never before, and never with sub-millisecond precision. ✅ Don't rely on timers for exact timing; treat the delay as a minimum.
Pro Tips
- 💡 Defer with setImmediate, not nextTick, when you just want to "let the loop breathe" between chunks of work — it won't starve I/O.
- 💡 Call timer.unref() on a background timer so it won't keep your process alive on its own.
- 💡 Use import { setTimeout } from 'node:timers/promises' to await a delay cleanly instead of wrapping setTimeout in a promise by hand.
📋 Quick Reference — Scheduling & Order
Scheduler
Runs
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
Frequently Asked Questions
🎉 Lesson Complete!
- ✅ setTimeout / setInterval schedule work; clearTimeout / clearInterval cancel it
- ✅ Order is: synchronous → process.nextTick → promise microtasks → timers → setImmediate
- ✅ setTimeout(fn, 0) is "very soon", not "now" — it's a macrotask
- ✅ process.nextTick beats timers but can starve the loop if abused
- ✅ The event loop cycles through timers, pending, poll, check, and close phases
- ✅ Next lesson: Child Processes — run other programs from Node with exec and spawn
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
- Previous: The process Object
- Next: Child Processes