When multiple async requests are in flight, they can resolve out of order and overwrite newer UI state. Explain why this happens and how to prevent stale updates with cancellation or guards.
Async Race Conditions and Stale UI Updates
Use guided tracks for structured prep, then practice company-specific question sets when you want targeted interview coverage.
The core issue
Async calls can resolve out of order. If request A starts, then request B starts, B might finish first. When A finishes later, it can overwrite newer UI state with stale data.
Step | What happens |
|---|---|
User types 'rea' | Request A is sent |
User types 'react' | Request B is sent |
Request B returns first | UI shows results for 'react' |
Request A returns later | UI is overwritten with stale 'rea' results |
How to prevent it
You need either real cancellation or a stale-result guard. Both prevent old responses from winning the race.
Technique | How it works | Notes |
|---|---|---|
AbortController | Cancel the previous request so it never resolves | Best when the API supports AbortSignal (e.g., fetch) |
Request id guard | Only apply results if the id matches the latest | Works even if the API cannot be cancelled |
takeLatest / switchMap | Wrap calls to auto-cancel or ignore stale results | Common in RxJS or custom utilities |
let requestId = 0;
async function search(query) {
const id = ++requestId;
const res = await fetch(`/api?q=${query}`);
const data = await res.json();
if (id !== requestId) return; // stale result
render(data);
}
let controller;
async function search(query) {
if (controller) controller.abort();
controller = new AbortController();
const res = await fetch(`/api?q=${query}`, { signal: controller.signal });
const data = await res.json();
render(data);
}
Pitfalls
- Promise.race does not cancel the losing promises.
- Debounce reduces request count but does not prevent out-of-order responses.
- Always clean up abort listeners or timers to avoid leaks.