AbortSignal is a runtime capability that tells cooperative work when to stop. An annotation can require a signal parameter, but only runtime checks and listener cleanup implement cancellation. Review JavaScript intermediate’s pending cancellation example if this behavior is unfamiliar.
Some APIs use rejection for cancellation. Others return a distinct cancelled variant. The following Outcome<T> chooses the latter for its own adapter; the underlying work can still throw or reject. Decide the public policy before writing catch logic.
Check a pre-aborted signal before invoking work, then pass the signal into the callback so it can cooperate. Check again when the awaited work settles, on both success and failure. This adapter gives cancellation priority if the signal is aborted at either settlement boundary; a different API could preserve the original outcome instead. It still waits for the callback to settle.
Do not promise that a callback ignoring the signal is interrupted. A typed function parameter is a caller contract, not a scheduler. The final catalogue project separately demonstrates stale-result rejection, without claiming that it cancels transport work.
Predict, run, and explain
Write the output and trace the important state changes before checking it. Then run the program. Use Check types first; execution is allowed only when the real compiler accepts the source.
type Outcome<T> =
| {
tag: "ok";
value: T;
}
| { tag: "cancelled" };
async function run<T>(
work: (signal: AbortSignal) => Promise<T>,
signal: AbortSignal
): Promise<Outcome<T>> {
if (signal.aborted) {
return { tag: "cancelled" };
}
const value = await work(signal);
if (signal.aborted) {
return { tag: "cancelled" };
}
return { tag: "ok", value };
}
// This introductory version assumes work succeeds; add catch in the exercise.
const controller = new AbortController();
controller.abort();
const result = await run(async () => 25, controller.signal);
console.log(result.tag);Enable JavaScript to edit and type-check this example.
Check your prediction
cancelled Cancellation policy includes the settlement boundary
Passing a signal lets a callback cooperate; it does not force it to stop. A public adapter must also decide whether to publish a value that fulfills after the caller aborted. This lesson gives cancellation priority at the settlement boundary while still waiting for work to settle.
Use controlled promises to test that choice. Pre-abort should prevent callback starts. Mid-flight abort should not claim the underlying callback already stopped, and it should not accidentally expose its late success if the policy says cancellation wins.
let release: (value: number) => void = () => {};
const pending = new Promise<number>(resolve => {
release = resolve;
});
const controller = new AbortController();
async function finish(): Promise<string> {
const value = await pending;
return controller.signal.aborted ? "cancelled" : String(value);
}
const work = finish();
controller.abort();
release(25);
console.log(await work);Enable JavaScript to edit and type-check this example.
Check your prediction
cancelled The callback completed with a real value, but the adapter deliberately suppressed it after checking ownership. This is observable policy, not proof the work was interrupted.
const controller = new AbortController();
controller.abort("stop");
let starts = 0;
async function work(signal: AbortSignal): Promise<number> {
signal.throwIfAborted();
starts++;
return 1;
}
try {
await work(controller.signal);
} catch (reason) {
console.log(String(reason));
}
console.log(starts);Enable JavaScript to edit and type-check this example.
Check your prediction
stop
0 The check occurs before the side effect. Moving it below starts++ would preserve the final cancellation result while violating the no-start contract.
Try the rule on new inputs
Test pre-abort, abort before fulfillment, abort before rejection and abort after the adapter has returned. Explain which outcomes can still change and why already completed side effects cannot be undone by a signal.
Your turn
Typed Cancellation: practise
Implement Outcome<T> with ok, cancelled and error variants. Write run(work,signal), passing the signal to work and applying the cancellation-priority rule on success and failure. Convert an ordinary thrown Error to an error message. Test pre-abort starts no callback, normal success, abort followed by success, cooperative abort rejection, and an ordinary error. Print cancelled 0, 25, cancelled, cancelled and offline.
// Catch unknown values safely, and state which condition chooses cancellation.Enable JavaScript to edit and type-check this example.
Hint 1: the idea
Checking before invocation prevents work from starting for an already-aborted request.
Hint 2: a first step
Return a cancelled variant before calling work. After awaiting work(signal), inspect signal.aborted again; do the same in catch before formatting an error.
Show one solution
type Outcome<T> =
| {
tag: "ok";
value: T;
}
| { tag: "cancelled" }
| {
tag: "error";
message: string;
};
async function run<T>(
work: (signal: AbortSignal) => Promise<T>,
signal: AbortSignal
): Promise<Outcome<T>> {
if (signal.aborted) {
return { tag: "cancelled" };
}
try {
const value = await work(signal);
return signal.aborted ? { tag: "cancelled" } : { tag: "ok", value };
} catch (error) {
if (signal.aborted) {
return { tag: "cancelled" };
}
return {
tag: "error",
message: error instanceof Error ? error.message : "Failed"
};
}
}
let starts = 0;
const stopped = new AbortController();
stopped.abort();
const a = await run(async () => {
starts++;
return 1;
}, stopped.signal);
console.log(a.tag, starts);
const b = await run(async () => 25, new AbortController().signal);
console.log(b.tag === "ok" ? b.value : b.tag);
const ignored = new AbortController();
const c = await run(async () => {
ignored.abort();
return 99;
}, ignored.signal);
console.log(c.tag);
const cooperative = new AbortController();
const d = await run(async signal => {
cooperative.abort();
signal.throwIfAborted();
return 99;
}, cooperative.signal);
console.log(d.tag);
const e = await run(async () => {
throw new Error("offline");
}, new AbortController().signal);
console.log(e.tag === "error" ? e.message : e.tag);Enable JavaScript to edit and type-check this example.
cancelled 0
25
cancelled
cancelled
offlineExplain why the implementation meets the stated contract. Which part is enforced by the language or checker, and which part needs behavioral evidence?
Change it and test it
Hold a callback on a deferred promise, abort while it is pending, then release it. Confirm the adapter waits for settlement and returns cancelled rather than publishing the value. Repeat with a rejection. A callback can ignore its signal parameter, so this policy prevents publishing its late result but cannot undo its side effects or force it to stop. An abort after run has already returned cannot retroactively change its result.
Completion is saved in this browser only. It does not measure mastery.