const log = [];
let currentResult = null;
function fakeSearch(query, delay) {
return new Promise(resolve =>
setTimeout(() => resolve(results for: ${query}), delay)
);
}
// Bug: no cancellation — fast response after slow can overwrite
async function search(query) {
const result = await fakeSearch(query, query.length < 6 ? 50 : 10);
currentResult = result; // always sets, even if stale
log.push('set: ' + result);
}
search('react'); // slow (50ms) — starts first
search('react hooks'); // fast (10ms) — starts second
setTimeout(() => {
console.log(currentResult); // race condition: 'react' might win!
}, 100);const log = [];
let currentResult = null;
function fakeSearch(query, delay) {
return new Promise(resolve =>
setTimeout(() => resolve(results for: ${query}), delay)
);
}
// Fix: request ID — only accept the latest request's result
let latestRequestId = 0;
async function search(query) {
const requestId = ++latestRequestId;
const result = await fakeSearch(query, query.length < 6 ? 50 : 10);
if (requestId === latestRequestId) { // still the latest?
currentResult = result;
log.push('set: ' + result);
} else {
log.push('discarded: ' + result);
}
}
search('react');
search('react hooks');
setTimeout(() => console.log(currentResult), 100);Bug: 'react' (50ms) starts before 'react hooks' (10ms). 'react hooks' resolves first and sets the result. Then 'react' resolves and overwrites with stale data.
Explanation: 'react' gets requestId=1, 'react hooks' gets requestId=2 (latestRequestId). 'react hooks' resolves first — requestId(2) === latestRequestId(2), sets result. 'react' resolves — requestId(1) !== latestRequestId(2), discarded.
Key Insight: Race condition fix: increment a counter on each request, check if yours is still the latest when it resolves. AbortController is cleaner (actually cancels the request) but request ID is simpler to understand.