Lesson 8 of 30
Firing a new request before the previous one for the same logical query has resolved lets responses arrive out of order — and an older, slower response can overwrite a newer, correct one unless something explicitly stops it.
12 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
Question 1 of 3
A search box fires a request on every keystroke. The request for "r" resolves after the request for "rea" because of network timing. What causes the resulting bug?
Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
A search box that fires a new request on every keystroke is the classic case: type "r", "re", "rea", and three requests are now in flight for the same logical query. Network timing is not guaranteed to match request order — the "r" request can take longer than the "rea" request and resolve last. If nothing accounts for this, the slow, stale "r" response arrives after the correct "rea" response and overwrites it, and the UI ends up showing results for a query the user no longer has typed.
There are two real, concrete fixes:
AbortController. Each new request gets its own controller; starting a new request explicitly aborts the previous in-flight one for the same logical query first. If the aborted request's response somehow still arrives, it's ignored because it was cancelled.useEffectfunction useSearchResults(query: string) {
const [results, setResults] = useState<Result[]>([]);
useEffect(() => {
if (!query) return;
const controller = new AbortController();
fetch(`/api/search?q=${encodeURIComponent(query)}`, { signal: controller.signal })
.then((res) => res.json())
.then((data) => setResults(data))
.catch((err) => {
if (err.name !== "AbortError") throw err; // a real failure, not a cancellation
});
// Cleanup runs before the next effect fires (i.e. before the next
// request starts) AND on unmount — both cases should cancel this one.
return () => controller.abort();
}, [query]);
return results;
}
Where AbortController isn't available or convenient (a library call that doesn't expose a signal, for instance), the sequence-number version does the same job without cancellation:
function useSearchResults(query: string) {
const [results, setResults] = useState<Result[]>([]);
const latestId = useRef(0);
useEffect(() => {
if (!query) return;
const requestId = ++latestId.current;
searchLibrary.search(query).then((data) => {
if (requestId === latestId.current) {
setResults(data); // still the latest request — safe to apply
}
// else: a newer request has since started, so this response is stale — drop it
});
}, [query]);
return results;
}
fetch throws, and that throw needs to be told apart from a real failure. Catching every error identically and showing "search failed" on an intentional cancellation is a real, easy-to-ship bug — check the error's abort signal before treating it as a genuine failure.AbortController is strictly better when it's available, because it can actually cancel the in-flight work."A user types quickly in a search box and sometimes sees results for an earlier, different query — what's happening and how do you fix it?" is a real, common debugging question, and naming the out-of-order-arrival cause plus a concrete fix (AbortController or a sequence check) is what separates a candidate who's hit this bug from one who hasn't.