useLazyQuery hook state can describe an older, aborted execution after a newer execution succeeds

Author: slydorCreated Sep 11, 2026Updated Sep 17, 2026
Labels:wilted_flower: needs-reproduction

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... variables contains 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:

  1. That execution was already aborted by a subsequent execute() call
  2. A newer execution has completed successfully
  3. 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:

typescript
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 useRef keyed by the input value, and do not read variables/data from 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