When multiple async operations can overlap with replacement/teardown (UI unmount, session rebuild, polling vs actions, stop/dispose, in-flight RPCs), require explicit ownership and lifecycle binding:
1) Claim ownership synchronously before the first await
2) Bind every async result to an identity token (generation / requestId / sessionId / tabId)
3) Serialize or coalesce work per scope
4) Cancellation-aware completion + deterministic cleanup
Code sketch (claim-before-await + generation + serial coalescing)
let pending: null | (() => void) = null;
let pendingToken = 0;
let lastApplied = 0;
async function resolvePendingWithAttachments(
tokenAtCall: number,
deliver: () => Promise<void>,
): Promise<boolean> {
const resolve = pending;
if (!resolve) return false;
// Claim synchronously
pending = null;
const myToken = ++pendingToken;
try {
await deliver();
// Bind to identity: only apply if still current
if (myToken !== pendingToken) return true;
lastApplied = tokenAtCall;
resolve();
return true;
} catch {
// Release the claim if delivery fails so retries can happen
if (myToken === pendingToken) pending = resolve;
throw;
}
}
// Polling/action coalescing pattern
let inflight: Promise<void> | null = null;
let rerunRequested = false;
async function refresh(reason: string) {
if (!enabled) return;
if (inflight) { rerunRequested = true; return; }
inflight = (async () => {
do {
rerunRequested = false;
const currentReason = reason;
const response = await fetchState(currentReason);
apply(response); // only after request completes
} while (rerunRequested);
})();
try { await inflight; } finally { inflight = null; }
}
Adopting this standard prevents: