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 setTimeout and thought JavaScript “paused” the whole page
  • Beginners confused by log order: A, C, then B
  • Anyone who left setInterval running 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

  1. Reproduce A / C / B log order with setTimeout.
  2. Build a counter with setInterval and a Stop button.
  3. Break setTimeout(fn(), …) on purpose; fix it.
  4. 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.