Timers, Cancellation & Async Patterns
Advanced · 18 min read · ▶ live playground · ✦ checkpoint
Starting async work is easy. The grown-up skill is stopping it, spacing it, and reading values that arrive over time. Timers, cancellation, debounce, throttle, and async iteration are the small patterns that keep async JavaScript from turning into noise.
Timers Need Handles
A timer handle is the value the runtime gives you so you can cancel a timer later. setTimeout schedules one future callback. setInterval schedules a repeating callback, so it must have a clear stopping rule.
function scheduleReminder(player, delay) {
const timeoutId = setTimeout(() => {
console.log(`${player}: stretch break`);
}, delay);
return () => {
clearTimeout(timeoutId);
console.log(`${player}: reminder cancelled`);
};
}
const cancelMinaReminder = scheduleReminder("Mina", 40);
scheduleReminder("Rae", 25);
setTimeout(cancelMinaReminder, 10);
let pulseCount = 0;
const pulseId = setInterval(() => {
pulseCount += 1;
console.log(`pulse ${pulseCount}`);
if (pulseCount === 3) {
clearInterval(pulseId);
console.log("pulse stopped");
}
}, 15);→ Mina: reminder cancelled, pulse 1, Rae: stretch break, pulse 2, pulse 3, then pulse stopped.
Two rules keep timer code sane:
- Store the handle if you may need to cancel it.
- Every interval owns its exit condition. An interval without
clearIntervalis a tiny memory leak with a clock.
Cancellation Is a Contract
Cancellation means telling async work you no longer want its result. A timeout can be cancelled with clearTimeout. A promise cannot be magically erased after it starts; the work itself must know how to stop or ignore its result.
For real web requests, the standard shape is AbortController, an object that creates a cancellation signal you pass into an async operation. The controller is the handle; the signal is the message.
This is the real browser/API shape for cancelling a request. It is static here because this lesson's live console does not make network requests.
const controller = new AbortController();
const scoreRequest = fetch("/api/scores/mina", {
signal: controller.signal,
});
setTimeout(() => {
controller.abort();
}, 50);
try {
const response = await scoreRequest;
console.log(`status: ${response.status}`);
} catch (error) {
if (error.name === "AbortError") {
console.log("score request cancelled");
} else {
throw error;
}
}You will meet fetch fully in Part IV. For today, keep the pattern: when you start cancelable work, keep the cancel handle near the code that may decide "we don't need this anymore."
Debounce vs. Throttle
Debounce means wait until calls stop for a quiet period, then run once with the latest value. Use it for search boxes and autosave: the user may type ten letters, but you want one final request.
Throttle means run at most once per time window. Use it for progress, scroll, resize, or repeated sensor updates: the stream is noisy, but you still want regular samples.
function debounce(action, wait) {
let timeoutId;
return value => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
action(value);
}, wait);
};
}
const recordScroll = debounce(position => {
console.log(`scroll saved: ${position}`);
}, 25);
setTimeout(() => recordScroll(10), 10);
setTimeout(() => recordScroll(50), 20);
setTimeout(() => recordScroll(90), 30);→ scroll saved: 90.
Try it — tame a noisy stream
Run this and compare the two tools. The search waits for quiet. The progress logger samples while the stream keeps moving.
The output is progress: 10%, search: min, progress: 90%, then search: mina. Debounce waits for quiet. Throttle lets the first call through, blocks the noisy middle, then lets a later call through after the window resets.
Async Iteration Reads Later Values
An async iterable is a value you can loop over with for await...of, where each next item may arrive later. An async generator is an async function that yields values over time; generators get their full model in lesson 13.1.
function delay(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}
async function* scoreFeed() {
const updates = [
{ player: "Mina", score: 41, delay: 10 },
{ player: "Rae", score: 38, delay: 20 },
{ player: "Nico", score: 44, delay: 30 },
];
for (const update of updates) {
await delay(update.delay);
yield update;
}
}
async function printFeed() {
for await (const update of scoreFeed()) {
console.log(`${update.player}: ${update.score}`);
}
console.log("feed closed");
}
printFeed();→ Mina: 41, Rae: 38, Nico: 44, then feed closed.
Use for await...of when the data itself is a sequence over time: streamed rows, chunks, messages, or progress updates. If you only need one future value, a normal promise is still the cleaner shape.
Section 12 gave you the async toolbox: callbacks, promises, await, combinators, timers, cancellation, and streams of values. Next, Section 13 turns to power features like iterators, generators, and Symbol protocols, which explain how patterns like for await...of fit into the language itself.
Checkpoint
Answer all three to mark this lesson complete