useLazyQuery hook state can describe an older, aborted execution after a newer execution succeeds
Issue Description
Summary
When useLazyQuery is called twice in quick succession, and the first call's network response lands after the second call's response (a race condition that occurs naturally during network jitter or slow first requests), the hook's variables and data fields end up describing the first (already-superseded) execution instead of the second, most-recent one. This violates the documented contract.
Expected behavior (from Apollo docs)
From the Apollo Client 4 useLazyQuery documentation:
When you call the execute function, it aborts in-flight queries from previous execute calls.
The lazy query hook's result object...
variablescontains the value of the variables from the last execution of the query.
Both statements imply that the hook should always reflect the state of the most-recent execution, not a stale earlier one.
Actual behavior
The hook's variables/data can remain pinned to an earlier execution even when:
- That execution was already aborted by a subsequent
execute()call - A newer execution has completed successfully
- The newer execution's response has been processed into the cache
The race window is: after the second execution completes (populating the hook), but before the first execution's network response arrives and is processed.
Reproduction
Two overlapping useLazyQuery calls with a realistic timing gap:
const [executeQuery] = useLazyQuery(SOME_QUERY);
// Call #1
void executeQuery({ variables: { input: "first" } });
// Pause > debounce delay (simulating network or user delay)
await delay(1150);
// Call #2 — Apollo aborts #1 per docs
void executeQuery({ variables: { input: "second" } });
// If #1's response now arrives before returning from this function,
// the hook ends up with variables/data both describing "first"
// instead of "second" (the last execute call).This occurs naturally in production when:
- A search/autocomplete field is debounced, and a user types, pauses, then types again
- The first request is slow but then resolves out-of-order
- Network conditions cause response reordering
Real-world impact
A production case: an address autocomplete field where debouncing spaces two queries >1000ms apart. If the first query's response arrives after the second query has already completed successfully, the UI displays suggestions for the wrong query term — a direct data integrity issue.
Root cause
The hook implementation does not enforce ordering of state updates. When two executions overlap, whichever network response is processed last wins, regardless of invocation order. The AbortController.abort() call on the earlier request does not prevent its already-in-flight response from updating the hook.
Proposed fix
The hook should track the execution order (e.g., a generation ID or timestamp) alongside each operation, and only apply state updates from the current-generation execution, ignoring updates from earlier ones that arrived late.
Notes
- This is distinct from any debounce library behavior; debounce reduces call frequency but does not prevent overlapping calls.
- The race is not hypothetical: it reproduces deterministically in a controlled test using real network delays and a real GraphQL server.
- A workaround exists: store the latest request/response pair in a
useRefkeyed by the input value, and do not readvariables/datafrom the hook itself.
Link to Reproduction
https://gist.github.com/slydor/efe1a48389f58eb85be2d81270a9d5fa
Reproduction Steps
Sorry, I wasn't sure I can use the error-template as that is based on v3. This one is pretty hard to reproduce as it requires real network timings and I wasn't able to get this done via some test framework. And yes, I admit to have used AI generating this repro and issue summary. It reproduced for me in Ubuntu 22.04.5 LTS with node 24.19.0 and @apollo/[email protected].
Terminal 1:
node apollo-repro-server.mjs
Terminal 2:
node apollo-repro-client.mjs
@apollo/client version
4.2.10
Source: apollographql/apollo-client