JavaScript / Editorial solution guide
Async results arrive in the wrong order or errors disappear
Diagnose stale search results, detached promise chains, and incomplete cleanup with controlled completions. Repair dependency ownership rather than adding delays until a test passes.
Read-only knowledge base ยท No public comments
Recognize the symptoms
An older query replaces a newer result, a spinner disappears too early, or a success message appears even though saving later rejects. A local catch may never run, and repeated retries can make the behavior seem intermittent. Preserve request identity and action/completion order before changing code.
Understand the likely causes
- Common causes are a missing return in then, an unawaited rejecting operation, async callbacks passed to forEach, and state updates without a current-request guard. Promise.all rejection does not cancel remaining work. Browser cancellation does not prove a server write was undone, so a retry policy must also respect operation semantics.
1. Reconstruct dependencies and identities
Label the initiating action, each launched promise, its observer, and the state it is allowed to change. Record a compact synthetic timeline of start, fulfill/reject, render, and cleanup. Ask whether the caller awaits the actual operation or merely the function that started it. If a callback calls save but returns nothing, the next stage receives undefined and can proceed early. A useful reproducer controls completion explicitly; fixed sleeps change probability without establishing why the order is wrong.
2. Connect completion and failure
Return the next promise from then, or await it where the current layer owns recovery. Use for...of with await for intentional sequence, and map plus an awaited aggregate for intentional overlap. Place try/catch around awaited work, and let unexpected defects propagate with useful context. A catch that logs and returns normally has recovered the chain; if no valid fallback exists, rethrow. Finally should release owned state without returning a new value that hides the original outcome.
3. Guard all latest-request state updates
For a latest-query interface, capture a generation at start and compare it before success, error, and cleanup. The example uses one accepted result and then calls an older completion; only the current identity can append. This is an acceptance rule, not cancellation. A write workflow may instead need to account for every result, so do not discard an old payment outcome just because another action started. Define the domain policy before applying this pattern.
let current = 0;
const displayed = [];
function begin() {
const own = ++current;
return value => { if (own === current) displayed.push(value); };
}
const finishOld = begin();
const finishNew = begin();
finishNew("new");
finishOld("old");
console.log(displayed.join(","));
new
The old completion still occurs but has no permission to update this latest-query display.
4. Add supported cancellation without false guarantees
An owned AbortController can stop supported fetch waiting or remove supported listeners, while a generation guard prevents late results from changing the active view. Handle expected abort separately from unexpected failure and still clean up the correct owner. Do not assume Promise.race with a timeout cancels the losing operation. For ambiguous writes, reconcile an operation identifier or use an agreed server idempotency contract before retrying. Keep cancellation, stale-result acceptance, and durable effects as separate questions.
Verify the fix
Use controlled fulfillment and rejection in both completion orders, then dispose the view before another result arrives. Verify the latest result, correct error message, and loading ownership. Add a deliberately rejecting save and assert no success appears before completion. Check that every launched promise has an observer and no unhandled rejection remains.
Keep it from recurring
- Make asynchronous return contracts explicit, centralize state transitions, and test reverse order rather than relying on timing luck. Document whether a workflow is latest-wins or every-result-matters. Retain a small reproducer for cancellation, error propagation, and stale cleanup whenever the behavior changes.