Recognize the ordering problem
Imagine a search field that starts a lookup after each change. Someone searches for “apple,” then quickly changes the query to “apricot.” If the apricot lookup finishes first and the apple lookup finishes later, a straightforward interface may show apple results under the newer query. Neither lookup has to fail for the display to be wrong.
This is an ordering problem, not necessarily a problem with slow requests. Network speed varies, but the same behavior can occur whenever asynchronous work finishes in a different order from the one in which it started. The interface needs a rule for deciding which completion is still allowed to change what the user sees. For a search field, that rule is often “only the most recently started search may update the results.”
Reproduce it without relying on network timing
You can demonstrate the issue in a modern browser’s developer console on any page where you can run JavaScript. The example below adds a paragraph to the page and replaces real requests with promises you resolve yourself. Enter the statements in order; no server, package, or test framework is required.
Create the controlled lookup and the first version of the search function: const pending = []; function lookup(query) { return new Promise(resolve => pending.push({ query, resolve })); } const output = document.createElement("p"); document.body.append(output); async function search(query) { const result = await lookup(query); output.textContent = result; }
Now run search("apple"); search("apricot"); pending[1].resolve("apricot results"); pending[0].resolve("apple results");. After the promise callbacks run, the paragraph shows “apple results,” even though apricot was the last search started. Resolving the promises in this order makes the bug repeatable. If you inspect pending, its query properties also show which stored resolver belongs to each search.
Give each search an identity
A small counter can enforce the latest-search rule. Replace the search function with this version: let latest = 0; async function search(query) { const ticket = ++latest; const result = await lookup(query); if (ticket !== latest) return; output.textContent = result; } Each call captures its own ticket. When its lookup finishes, it checks whether a newer call has increased latest. An older completion returns without changing the paragraph.
Try the same sequence again after clearing the demonstration state: pending.length = 0; output.textContent = ""; latest = 0; search("apple"); search("apricot"); pending[1].resolve("apricot results"); pending[0].resolve("apple results");. This time the paragraph stays on “apricot results.” You can reverse the resolution order, too: when the newest search finishes last, it still displays its result. The counter protects the intended behavior rather than depending on which request happens to be faster.
Apply the same rule to errors and loading states
Real lookups can reject as well as resolve. If an older search fails after a newer one succeeds, an unguarded error handler could replace valid results with an obsolete error message. Put the ticket check before updating the interface in both the success and error paths. The same principle applies to a loading indicator: an older search should not hide it while a newer search is still pending.
If a lookup uses fetch, AbortController can also help you cancel a previous request when a new search starts. Cancellation may reduce unnecessary work, but it is not a substitute for deciding which result may update the interface. A response or other asynchronous task may already be completing, and not every lookup supports cancellation. A latest-search check makes the display rule explicit regardless of how the work is performed.
Turn the reproduction into a regression check
A useful test controls two pending lookups rather than waiting for arbitrary delays. Start search A, start search B, resolve B, then resolve A. Check that B remains visible. In another case, resolve A first and B second; the final display should still be B. If the interface shows errors or loading feedback, repeat the out-of-order scenario with a rejected older lookup and check that it cannot overwrite the newer state.
Also decide what should happen when the query becomes empty. If clearing the field means clearing results, treat that action as a new state change: invalidate earlier searches before clearing the display. Otherwise, an earlier lookup could finish afterward and repopulate results for a query that is no longer present. These checks make the desired behavior precise and keep a timing-sensitive bug reproducible.



