Search-as-you-type has a classic failure: the user typed five letters, five requests fired, the second response arrives last, and its old results overwrite the correct fifth. Every fix ends at the same tool: AbortController.
let inFlight;
async function search(q) {
inFlight?.abort(); // cancel the previous query
inFlight = new AbortController();
try {
const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`, {
signal: inFlight.signal,
});
render(await res.json());
} catch (err) {
if (err.name !== "AbortError") throw err; // aborts are expected
}
}
// cleanup when the component or page closes
addEventListener("pagehide", () => inFlight?.abort());The four habits in that snippet
- One controller in flight: aborting it also unregisters its signal, so the old request stops eating bandwidth and CPU.
- AbortError is a normal state, not a crash: swallow it, because the canceled request did its job by being canceled.
- Debounce still helps: aborting stops the stale-result bug, debouncing stops requests that were pointless bytes from birth.
- Cancel on teardown too: navigation and component unmount leave fetches alive otherwise.
Where else it earns its bytes
- Tabbed content where each tab fetches: switching mid-load without the abort is the same race.
- Multi-file upload flows where the user should be able to cancel one file, not the whole batch.
- Polling loops that restart: a fresh interval must retire the previous request or two responses arrive and fight.
This site's search page intentionally needs no network: the index ships in-page, so the race cannot happen at all, which is the most gacor fix of all if your dataset fits. When it does not, aborting beats everything else, and the code is fifteen lines.
