Completion order is not intent order
A person searches for web, then JavaScript. The first request may finish last. Rendering every fulfillment lets an older result replace the requested one. A current-request token protects the commit point: increment a generation number for each load, remember it locally, and update state only when the remembered number is still current.
In const mine = ++generation, the prefix ++ increments generation first and then supplies that new number to mine. The longer equivalent is generation = generation + 1; const mine = generation;. Do not replace it with postfix generation++ in this assignment: that would capture the old number.
A stale failure must also be ignored, not only stale success. Otherwise the UI can show an old error on top of a newer success. An unconditional finally that clears loading can cause the same defect. Every asynchronous completion path that changes state must check ownership.
Cancellation can ask a previous request to stop, but a transport may ignore it. Keep the generation check even when you use AbortController. A token prevents stale commits; it does not stop network activity or undo a completed server-side effect.
To test ordering without sleeps, make a deferred promise: keep the resolve function and choose when to fulfill it. The example creates two requests, fulfills the newer one first, then the older one. This is a controlled schedule rather than a timing guess. The callback in the Promise constructor runs immediately, so resolve has been assigned when deferred returns.
Predict and trace
Write the output before running. Follow the values through each call; if your prediction differs, identify the first step that surprised you.
async function main() {
function deferred() {
let resolve;
const promise = new Promise(done => {
resolve = done;
});
return { promise, resolve };
}
let generation = 0;
let visible = "";
async function load(promise) {
const mine = ++generation;
const value = await promise;
if (mine === generation) {
visible = value;
}
}
const old = deferred();
const recent = deferred();
const a = load(old.promise);
const b = load(recent.promise);
recent.resolve("new");
await b;
old.resolve("old");
await a;
console.log(visible);
}
main();Check your prediction
newThe older completion is observed but cannot commit. Remove the comparison to see which output would reveal the defect.
Protect failure paths as well as successful results
A stale completion can be a rejection, not just a successful response. If an old error replaces a newer success, the interface is still wrong. The request identity must guard every asynchronous path that changes visible state.
Allocate the identity before starting or awaiting the dependency. Keep it captured for that attempt. Cancellation can reduce wasted work, but request ownership remains necessary when a dependency ignores a signal or finishes before cancellation takes effect.
async function main() {
let generation = 0;
let visible = "";
let rejectOld;
const old = new Promise((resolve, reject) => {
rejectOld = reject;
});
async function load(work) {
const mine = ++generation;
try {
const value = await work;
if (mine === generation) {
visible = value;
}
} catch {
if (mine === generation) {
visible = "error";
}
}
}
const first = load(old);
await load(Promise.resolve("new"));
rejectOld(new Error("old failure"));
await first;
console.log(visible);
}
main();Check your prediction
new The old rejection has a handler attached before it is released. Its captured identity is stale, so it cannot replace new with error. No timer delay is needed to create this schedule.
let current = 2;
let state = "loading new";
function finish(request, value) {
if (request === current) {
state = value;
}
}
finish(1, "old");
console.log(state);
finish(2, "new");
console.log(state);Check your prediction
loading new
new A completion must also be ignored while the newer request is still loading. Testing only after the newer success can miss a guard that happens to rely on a ready-state check instead of request identity.
Try the rule on new inputs
Test old success and old failure both before and after the new completion. Remove one identity comparison and identify which schedule exposes it. Keep loading ownership separate from transport cancellation.
Build it yourself
Request Ordering: practise
Write createGate(), with next() returning an increasing token and isCurrent(token) testing ownership. Print whether the first and second tokens are current after two starts.
// The generation belongs to one gate instance.Hint 1: the idea
This is a closure with one changing number.
Hint 2: a first step
Increment in next, compare in isCurrent.
Show one solution
function createGate() {
let generation = 0;
return {
next() {
generation += 1;
return generation;
},
isCurrent(token) {
return token === generation;
}
};
}
const gate = createGate();
const first = gate.next();
const second = gate.next();
console.log(gate.isCurrent(first));
console.log(gate.isCurrent(second));false
trueStarting a third request invalidates both earlier tokens. Decide how cancel should invalidate the current token even when no replacement load starts.
Test a changed requirement
Starting a third request invalidates both earlier tokens. Decide how cancel should invalidate the current token even when no replacement load starts.
Explain before moving on
Which three completion paths need an ownership check? What does a token not cancel?
Completion is saved in this browser only. It does not measure mastery.