# cooperative cancellation: a worked example

Canonical page: https://vibe-coding-101.brighteng.org/c/cancellation/#before-and-after

## The problem

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.

## Prompt to paste

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

## 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.

## 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.

## Run the check

Save the following code as cancellation.mjs and run `node --test cancellation.mjs` with Node.js 22.12 or later.

```js
import test from 'node:test';
import assert from 'node:assert/strict';

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

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

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']);
});
```

## Expected result

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.

## Limits

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.

## Sources

- [MDN: AbortSignal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal): Signal propagation, throwIfAborted, one-use signals, and cooperative APIs.
- [MDN: AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController): Aborting supported requests is distinct from clearing a timer.

Sources checked: 2026-10-11. These are original illustrative examples with automated checks. Human editorial approval is not recorded. Policy: https://vibe-coding-101.brighteng.org/editorial/
