What you'll be able to do
By the end of this page you should be able to schedule work with setTimeout and setInterval, explain why a console.log after setTimeout often prints first, pass a function as a callback, and clear timers so they do not keep firing forever.
Previous post: modules in the browser. This opens Phase 8 — async.
Who this is for
- People who wrote
setTimeoutand thought JavaScript “paused” the whole page - Beginners confused by log order: A, C, then B
- Anyone who left
setIntervalrunning and wondered why the fan spun up
You can skip this if timer callbacks already feel obvious. Come back when a nested callback pyramid starts to hurt — that pain leads into promises next.
Sync first, then “later”
JavaScript on a page runs one main thread of work. Most of your earlier blogs were synchronous: line 1 finishes, then line 2. Timers and network calls are different — you schedule work for later, and the engine keeps going.
Scenario A — cooking while water boils. You set a timer for the pasta, then chop vegetables. You do not stand frozen until the timer rings.
Scenario B — a button that waits two seconds. You schedule a toast message; the user can still click other buttons meanwhile.
setTimeout: run once after a delay
console.log("A — start");
setTimeout(function () {
console.log("B — later");
}, 1000);
console.log("C — still going");
// Typical order: A, C, then B after ~1 second
Scenario A — debounce a “Saved” message. Wait 300ms after typing stops before showing a tip (full debounce pattern comes in Phase 10).
Scenario B — fake loading. Show “Working…” then swap to “Done” after a short delay in a demo — not a substitute for real fetch later.
The delay is a minimum, not a promise of exact milliseconds. The browser may be busy with other work.
Callbacks: “call this function when ready”
A callback is just a function you pass so someone else can call it later — you already did that with addEventListener("click", handler).
function afterDelay(ms, fn) {
setTimeout(fn, ms);
}
afterDelay(500, function () {
console.log("half a second later");
});
Scenario A — timer API. setTimeout takes your callback.
Scenario B — array methods. map(function (item) { ... }) is also a callback — but it runs now, in the same turn, not after a delay. Same idea (pass a function), different timing.
setInterval: repeat until you stop it
let count = 0;
const id = setInterval(function () {
count += 1;
console.log(count);
if (count >= 5) clearInterval(id);
}, 1000);
Scenario A — live clock on a page. Update a clock every second.
Scenario B — polling (check a status every few seconds). Prefer clearer patterns later with fetch; for now know that intervals stack if the work takes longer than the gap — beginners often forget clearInterval.
Always keep the id from setInterval / setTimeout if you might cancel.
Why the log order feels “wrong”
console.log(1);
setTimeout(() => console.log(2), 0);
console.log(3);
// 1, 3, 2 — even with 0 ms
Scenario A — zero delay. 0 means “as soon as the current stack finishes,” not “immediately before the next line.”
Scenario B — interview puzzle. People expect 1, 2, 3. The timer callback waits in a queue until the sync code is done. The event loop (Phase 9) names this formally; for today, remember: finish the current script first, then run due timers.
clearTimeout / clearInterval
const id = setTimeout(() => console.log("never"), 5000);
clearTimeout(id);
Scenario A — user navigates away. Clear pending work so it does not touch a removed DOM node.
Scenario B — “cancel reminder” button. One click clears the pending toast.
A tiny practice file
Save as timers.html.
<!DOCTYPE html>
<html>
<body>
<button id="once">Say hi in 1s</button>
<button id="tick">Start counter</button>
<button id="stop">Stop counter</button>
<pre id="out"></pre>
<script>
const out = document.getElementById("out");
let intervalId = null;
document.getElementById("once").addEventListener("click", function () {
out.textContent = "waiting…";
setTimeout(function () {
out.textContent = "Hello after 1 second";
}, 1000);
});
document.getElementById("tick").addEventListener("click", function () {
let n = 0;
if (intervalId) clearInterval(intervalId);
intervalId = setInterval(function () {
n += 1;
out.textContent = "tick " + n;
}, 500);
});
document.getElementById("stop").addEventListener("click", function () {
if (intervalId) clearInterval(intervalId);
intervalId = null;
out.textContent = "stopped";
});
</script>
</body>
</html>
Mistakes I see a lot
1. Thinking setTimeout freezes the whole page. It schedules; sync code keeps running.
2. Expecting exact order with 0 delay. Sync lines still win first.
3. Nested callbacks for multi-step “then do this, then that.” Readable for one step; painful for five — promises next.
4. Forgetting clearInterval. Counters and polls keep running after you leave the screen.
5. Passing setTimeout(fn(), 1000) with parentheses. That calls fn now and passes its return value — usually undefined. Pass the function: setTimeout(fn, 1000).
6. Using intervals for animation without checking requestAnimationFrame. Fine for learning; real UI animation often prefers rAF later.
What to try before the next post
- Reproduce A / C / B log order with
setTimeout. - Build a counter with
setIntervaland a Stop button. - Break
setTimeout(fn(), …)on purpose; fix it. - Build
timers.html.
Next in this series: promises plainly — pending / fulfilled / rejected, and .then / .catch without the mystery.
Try this next outside the series
Async code gets much easier when you see the same ideas on the browser side and the server side.
- Node.js async patterns — see promises and async functions in real backend flows
- Node.js streams intro — continue from simple async waits into data that arrives over time