Skip to catalogue

cooperative cancellation

Work must observe cancellation and stop safely.

Use this when An old search or upload keeps running after the user leaves.

without the term

“If I type a search and then quickly type another, sometimes the results for the first search show up last and overwrite the right ones. Can you make old searches stop or get ignored?”

with the term · How do you ask a model for cooperative cancellation?

Make this operation cooperatively cancellable. Propagate cancellation and verify cancelled or obsolete work cannot update the UI.

XBlueskyLinkedIn

What is cooperative cancellation?

In cooperative cancellation, a cancellation request signals intent; the running operation must check it or use cancellation-aware APIs.

Why does cooperative cancellation matter when vibe coding?

Calling cancel alone does not guarantee that obsolete work stops or cannot publish a result.

How do you do cooperative cancellation?

Propagate cancellation, check it between work units, and prevent obsolete completions from updating current state.

What goes wrong with cooperative cancellation?

A catch-all handler swallows cancellation and continues the operation.

Before and after

A user leaves a search screen while its request is in flight. Cancelling the owner should prevent a late completion from updating the abandoned screen, including when a dependency ignores the signal.

Before

The owner creates an AbortController but never passes its signal to the dependency or checks it before publishing. Calling abort changes the signal, while the pending work can still update the view.

async function before(load, signal, publish) {
  const result = await load();
  publish(result);
}

After

Pass the signal through the supported boundary, check before starting, and check again after awaiting the result. The second check prevents publishing a late result even if that particular dependency did not cooperate.

async function after(load, signal, publish) {
  signal.throwIfAborted();
  const result = await load({ signal });
  signal.throwIfAborted();
  publish(result);
}

Check the change

Calling abort alone leaves the unguarded publication intact. The revised version passes the exact signal, rejects with AbortError after a cancelled late completion, does not start already-cancelled work, and still publishes an active result.

Save the runnable comparison, then run node --test cancellation.mjs with Node.js 22.12 or later.

Download the complete teaching example: the bug, prompt, runnable code, expected result, limits, and sources, linked back to this page.

Show the assertions
test('aborting an unused signal does not prevent publication', async () => {
  const controller = new AbortController();
  const shown = [];
  let finish;
  const task = before(() => new Promise((resolve) => { finish = resolve; }),
    controller.signal, (value) => shown.push(value));
  controller.abort();
  finish('obsolete');
  await task;
  assert.deepEqual(shown, ['obsolete']);
});

test('cancellation is propagated and late publication is blocked', async () => {
  const controller = new AbortController();
  const shown = [];
  let finish, received;
  const task = after(({ signal }) => {
    received = signal;
    return new Promise((resolve) => { finish = resolve; });
  }, controller.signal, (value) => shown.push(value));
  assert.equal(received, controller.signal);
  controller.abort();
  finish('obsolete');
  await assert.rejects(task, { name: 'AbortError' });
  assert.deepEqual(shown, []);
  let calls = 0;
  await assert.rejects(after(() => { calls++; }, controller.signal, () => {}),
    { name: 'AbortError' });
  assert.equal(calls, 0);
  const active = new AbortController();
  await after(async () => 'current', active.signal, (value) => shown.push(value));
  assert.deepEqual(shown, ['current']);
});

When this is not enough

Cancellation is cooperative: a dependency that ignores the signal can continue using resources. A signal does not undo a completed server write. Use a fresh controller for new work, handle cancellation separately from current errors, and release listeners you add yourself. A generation guard still helps when several active operations share a view.

The next decision

  • race condition: Choose which result owns the current view when operations overlap.
  • idempotency: Protect repeated effects separately; aborting a caller does not prove the server stopped.

Sources and verification

Sources checked . These are illustrative examples with automated checks. How we check content.

Did this help with your task?

After trying the prompt or check, tell us what happened. This optional response records only your choice and this term.

Choose between related terms

adjacent

Expanded example updated .

Historical short-entry review

report a correction