JavaScript / Advanced: scheduling and trustworthy interfaces / Lesson 14
Concurrency boundaries and result ownership
Coordinate overlapping operations without assuming promises provide parallel execution or cancellation. Use generations and transferred data to reason about stale results and worker boundaries.
On this page
Lesson goals
- Prevent stale operations from updating current state.
- Separate cancellation from result acceptance.
- Explain worker messages, cloning, and ownership transfer.
Single-threaded callbacks can still race logically
Two searches can overlap while their JavaScript callbacks run one at a time. If the earlier search finishes last, an unguarded callback can replace the newer result. This is a logical race across asynchronous boundaries, not necessarily simultaneous writes on the same thread. Define the product rule first: latest query wins, every operation matters, or results merge according to stable identifiers. A generation counter is suitable for latest-query display but would be wrong for independent financial operations that must all be accounted for. Choose coordination from the domain rather than applying one cancellation pattern everywhere.
Accept results only for the current generation
Increment a generation when starting a new search and capture that number in its continuation. Before displaying success, error, or cleanup, confirm it still owns the active request. The example deliberately resolves the newer request first and the older request second, with no timer or network. Only new is accepted. Guarding success alone is incomplete: an old rejection could still overwrite a current message, and an old finally could clear the newer loading indicator. Test all three paths under reversed completion order and after the view is disposed.
let generation = 0;
const shown = [];
function start(promise) {
const own = ++generation;
return promise.then(value => { if (own === generation) shown.push(value); });
}
let resolveOld, resolveNew;
const oldWork = start(new Promise(resolve => { resolveOld = resolve; }));
const newWork = start(new Promise(resolve => { resolveNew = resolve; }));
resolveNew("new");
await newWork;
resolveOld("old");
await oldWork;
console.log(shown.join(","));
new
Both operations finish, but only the current generation is allowed to affect the display record.
Cancellation is a cooperative operation contract
AbortController supplies a signal used by supporting web APIs, including fetch and event listeners. Calling abort requests the behavior defined by that API; it does not undo an already committed external effect or cancel every promise derived from the operation. Retain a stale-result guard when acceptance rules require it. Distinguish a user cancellation from an unexpected failure in the UI, and still release owned resources. For writes, server idempotency and transaction rules are separate safeguards. A browser abort cannot establish that the server never received or completed a request, so avoid declaring a payment safely nonexistent merely because local waiting stopped.
A worker boundary changes data ownership
A web worker runs in a separate execution environment and cannot directly manipulate the page DOM. Communicate through messages with a defined request identifier, payload schema, and result/error representation. Ordinary structured-cloned data is copied according to supported type rules; transferable objects can move ownership instead. Transferring an ArrayBuffer detaches it from the sender, so a sender that continues reading it violates its own assumptions. The example demonstrates transfer locally without creating a worker or loading a script. Large payload costs and result rendering still matter even when computation leaves the main thread.
const input = new Uint8Array([7, 9]);
const output = structuredClone(input, { transfer: [input.buffer] });
console.log(input.buffer.byteLength);
console.log(output.join(","));
0
7,9
The sender buffer is detached while the cloned view has the values. This validates ownership transfer, not worker scheduling.
Bound work and make completion meaningful
A worker protocol should reject unknown message shapes, associate results with the requesting view, and define termination. Do not send functions or DOM nodes through a structured-clone channel and assume they are portable data. Limit queued work and payload size; a worker can still exhaust memory. SharedArrayBuffer introduces shared-memory coordination and security requirements beyond ordinary copying, so it is not an automatic optimization. Prefer message ownership and immutable results when adequate. Test cancellation, late messages, malformed payloads, and worker failure using controlled local cases before adding real dependencies or making throughput claims.
Common errors and fixes
Old search results replace a new query
- Why it happens
- Completion order is mistaken for user-intent order.
- How to fix it
- Use request identity and guard success, error, and cleanup.
- Prevent it next time
- Test controlled reverse completion and disposal.
Abort is treated as undoing a write
- Why it happens
- Local cancellation is confused with server transaction state.
- How to fix it
- Reconcile operation identity and use server-side duplicate protection.
- Prevent it next time
- Document ambiguous completion explicitly.
The sender reuses a transferred buffer
- Why it happens
- Ownership transfer is mistaken for a regular copy.
- How to fix it
- Stop using detached storage or choose copying instead.
- Prevent it next time
- State payload ownership in the message contract.
Check your understanding
1. Why guard finally in a latest-search workflow?
Show worked solution
An older request can finish after a newer one starts and clear its busy flag. Cleanup should confirm the same generation still owns the active state. The same check belongs around errors and success so no stale path can replace the current interface state.
2. A worker needs a large buffer and the caller will not use it again. Which ownership choice can fit?
Show worked solution
A supported transfer can move the buffer rather than cloning its contents, with the sender treating the original as detached. Verify the receiving payload and lifecycle, and measure end-to-end cost. Transfer is not appropriate if the sender still needs the same storage.
Key takeaways
- Overlapping results require domain-specific acceptance and ownership rules.
- Promises, cancellation signals, and workers solve different problems.