Reviewed: 2026-09-06.
A response should update the interface only if it still belongs to the user’s current intent. Cancel obsolete work to save resources, and separately verify response identity because cancellation and completion are different concerns.
Reproduce the race
An autocomplete sends a request for “ca,” then one for “cat.” The second request returns first and shows cats. The older request returns later and replaces the list with results for “ca.” Nothing failed at the network level; the UI accepted a valid response in the wrong context.
Navigation creates the same bug when a user opens customer A and quickly opens customer B. Async validation can show “email available” for an earlier value. Writes are more serious: two autosaves can complete out of order and leave older content visible or persisted unless both client and server handle conflicts deliberately.
The Fetch API documentation explains that fetch() returns a Promise that resolves to a Response. To prevent stale UI updates, bind each result to the state that started it instead of assuming the next completion still matches current user intent.
Cancel obsolete reads
Create an AbortController for the current request and abort it when the query, route, or component changes. The AbortController documentation explains that its signal can abort fetch requests and consumption of response bodies.
Treat abort as an expected lifecycle event, not an error toast. Clean up loading indicators only if they still belong to the aborted request. Cancellation is still worth doing for efficiency, but it should not be your only correctness guard.
Check identity before committing state
Increment a sequence number or create a request ID for each intent. Capture it when starting work. Before applying success, failure, or completion state, compare it with the current ID. Ignore results from older identities.
Identity should include all inputs that affect the result: normalized query, tenant, locale, filters, page, and selected account. A cache keyed only by search text can return tenant A’s result in tenant B’s view. Central query libraries help only when keys are complete.
Keep data and status under the same identity. Otherwise a stale request can clear the spinner for a newer request or replace a newer error with an old success.
For navigation, bind the result to the route key and entity version. For field validation, bind it to the exact normalized value. If the user changes the field and changes it back, a keyed cache may reuse the earlier valid result only if its freshness and context remain acceptable.
Protect writes at the server
Client sequence checks prevent stale rendering but cannot fix a conflicting write that the server already accepted. Have the server verify that each mutation still matches the version the client edited, and reject or surface conflicts when it does not.
Autosave can serialize writes per document and coalesce intermediate states so only the newest useful state is sent. On conflict, fetch the current version and present a merge or retry flow that does not discard text. Do not rely on client-side cancellation alone as your write-safety mechanism; the server still needs conflict handling and idempotent mutation design where duplicates are possible.
Cover every terminal path
The latest request may succeed, fail, abort, or be superseded. Only the current request may set data, error, empty state, and loading false. A simple finally block without an identity check is a common source of flicker.
Test with deterministic delays: make request one take 500 milliseconds and request two take 50. Repeat for success-after-success, error-after-success, success-after-error, cancellation during the request lifecycle, route change, and component unmount. Assert both visible data and status.
Start with autocomplete because the failure is easy to see. Add AbortController cleanup for efficiency, a monotonically increasing request token for correctness, and a test in which the first response arrives last. Then audit writes so the server detects and handles conflicts instead of letting older state overwrite newer intent.
Sources are linked throughout this guide. Product capabilities can change; consult the linked documentation for your deployment.
Read our editorial approach ↗