What goes wrong
Every keystroke sends a request. HTTP responses are not guaranteed to arrive in the order they were sent: one may hit a cold cache, a slower server or a congested connection. In the trace above, the request for "pa" takes 1,240 ms and lands after the results for "paris" are already on screen. If the app writes whatever arrives, the screen goes backwards.
Debounce is not enough
Debouncing input to 200–300 ms reduces the number of requests, which is good, but it does not order them. Two debounced requests can still race. Debounce for load; use one of the fixes below for correctness.
Cancel the old request, or compare before writing
let controller;
async function search(q) {
controller?.abort();
controller = new AbortController();
const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`, { signal: controller.signal });
const data = await res.json();
if (q === input.value) render(data); // only render results for what the box says now
}
Aborting saves bandwidth; comparing against the current input value is the safety check that actually guarantees correctness. Do both.
The switchMap pattern
In RxJS, switchMap unsubscribes from the previous inner request whenever a new value arrives, which encodes "only the latest matters" directly:
fromEvent(input, "input").pipe(
map((e) => e.target.value),
debounceTime(200),
distinctUntilChanged(),
switchMap((q) => fetchResults(q)),
).subscribe(render);
Catch the ones you missed in production
Most apps have more than one search box, and filters, tabs and pagination fail the same way. GenClass detects a response that would overwrite newer data across every fetch in your app and, in guard mode, discards it before render. At the default setting it acts rarely; the benchmarks show how often.
npm install @genclass/runtime
Related: race conditions in React useEffect.