Callbacks and timers

JavaScript · Practical · 10 min

Asynchronous callbacks are the foundation that promises and async/await are built on.

JavaScript runs your code on a single thread, one piece at a time, and each piece runs to completion — nothing interrupts it. Slow operations (timers, network, disk, user input) don't block that thread. Instead you hand over a callback to be called later, and your code keeps going.

Timers are the simplest example:

  • setTimeout(fn, ms) calls fn once, after at least ms milliseconds.
  • setInterval(fn, ms) calls it repeatedly.
  • Both return an ID; cancel with clearTimeout(id) / clearInterval(id).

The delay is a minimum, not a guarantee: the callback runs only when the thread is free. Even setTimeout(fn, 0) runs after the current code finishes. Browsers also clamp nested timers to at least 4 ms and throttle timers in background tabs.

Event handlers, XMLHttpRequest and older Node.js APIs all use callbacks. Node's convention is the error-first callback: fs.readFile(path, (err, data) => { ... }) — check err first.

Callbacks have real drawbacks once operations depend on each other:

  • Nesting grows rightward into "callback hell".
  • Each level must handle errors manually; a throw inside a callback can't be caught by a try around the code that scheduled it.
  • It's easy to call a callback twice, or never.

Promises (next lesson) solve these by representing the future result as a value you can return, chain and combine. Timers themselves are still useful for delays, polling, and debouncing user input.

Example

console.log("1: start");

setTimeout(() => console.log("4: timeout 0ms"), 0);

let ticks = 0;
const id = setInterval(() => {
  ticks++;
  console.log(`tick ${ticks}`);
  if (ticks === 2) clearInterval(id);    // stop after two ticks
}, 10);

console.log("2: scheduled");

// A debounce: only run after calls stop for `ms`
function debounce(fn, ms) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), ms);
  };
}
const save = debounce(text => console.log("saved:", text), 50);
save("h"); save("he"); save("hello");     // only "saved: hello" prints
console.log("3: end of script");

Remember

  • Code runs to completion; callbacks run later when the thread is free
  • Timer delays are minimums; setTimeout(fn, 0) still waits for current code
  • Nested callbacks are hard to compose and to error-handle — promises fix that

Watch out for

  • Expecting setTimeout(fn, 1000) to fire exactly on time while heavy code is running.

Check yourself

  1. console.log("A"); setTimeout(() => console.log("B"), 0); console.log("C"); — what is the output order?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout