When arrival order lies: enforcing latest intent

When multiple requests are in-flight, responses can arrive out of order. Without protection, an older (slower) response can overwrite the user’s latest intent.

Break it: out-of-order responses overwrite UI

Type quickly to create overlapping requests. Requests complete with variable latency. Sometimes an older response arrives last and overwrites the latest intent.

Buggy (under test)

Buggy

out-of-order overwrites UI
Query
Type to search
Results
Trace (last 6)

Detected issue

Status: OK
Latest intent: "rea"
Last applied:
Risk: stale response may win
How to trigger it
  • Type fast enough to overlap requests (e.g., “r” → “re” → “rea”).
  • Requests complete with variable latency, so an older request can finish last.
  • Watch results show an older query after you typed a newer one
Buggy snippet
fakeSearch(query).then((res) => {
  setResults(res); // ❌ commits whichever response arrives last
});

Why this breaks

Mental model: concurrency breaks arrival order

Concurrency turns time into a correctness problem.

Time is now part of your state model.

Arrival order is not causality.

Correctness must be invariant to timing.

Invariant: UI must reflect the latest intent (latest input), not the latest arrival.

Arrival-driven commits allow obsolete intent to win.

This is a causality violation — the system applies effects out of order.

Requests overlap. Completion order becomes nondeterministic under concurrency.

If you commit whatever arrives last, your UI becomes “latest arrival wins”, which is not what the user wants.

This class of bug is extremely common in search boxes, filters, autosuggest, and collaborative UIs.

Reference (correct mental model)

Latest intent wins

Wrong mental model

“Last write wins” assumes responses arrive in order. With concurrency, that’s false.

// ❌ "latest arrival wins"
fakeSearch(query).then(setResults);

Correct mental model

UI should reflect latest intent, not latest arrival. You must enforce a policy: gate or cancel.

// ✅ "latest intent wins"
startRequest();
ignoreOrCancelOlder();

Fix

Minimal reliable fix: assign each request an incrementing id and only apply the result if it matches the latest request.

Correctness must not depend on network timing.

Fixed: latest wins (requestId gate)

Fixed

latest wins (requestId gate)
Query
Type to search
Results
Trace (last 6)
Fixed snippet
const id = ++requestIdRef.current;

fakeSearch(query).then((res) => {
  if (id !== requestIdRef.current) return; // ✅ ignore stale response
  setResults(res);
});
Rule of thumb
When requests overlap, never trust arrival order. Enforce intent.

Optional

Instead of only ignoring stale responses, you can cancel old requests. With real fetch, pass signal.

AbortController (cancel old requests)

Abort

cancels old requests (AbortController)
Query
Type to search
Results
Trace (last 6)
Abort snippet
abortRef.current?.abort();
const controller = new AbortController();
abortRef.current = controller;

fetch(url, { signal: controller.signal })
  .then(r => r.json())
  .then(setResults)
  .catch(err => {
    if (err.name === "AbortError") return;
    throw err;
  });

Key takeaways

  • Overlapping requests can finish out of order.
  • UI should reflect latest intent, not latest arrival.
  • Fix with gate (requestId) or cancel (AbortController).

Approach & tradeoffs

Approach

  • Assume concurrency: completion order is nondeterministic.
  • Pick a policy: gate (ignore stale) vs cancel (abort old).
  • Make the policy explicit in code, not implicit in timing.

Tradeoffs

Gate (requestId)

  • ✅ Simple. Works with any async primitive.
  • ⚠️ Prevents stale commits but does not stop work.

Cancel (AbortController)

  • ✅ Stops old work in real fetch.
  • ⚠️ Requires abortable API + abort handling.
Common pitfall

Assuming the latest response reflects the latest user intent.

It rarely does under concurrency.