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)callsfnonce, after at leastmsmilliseconds.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
throwinside a callback can't be caught by atryaround 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.