skip to content

Why does JavaScript have no promise.cancel(), and what does AbortController give you instead?

level: middleimportance: must knowfreq 58%

answer

  1. a handle to a result, not to work
  2. settle-once, many consumers
  3. the producer holds resolve and reject
  4. signal forwards, don't cancel backwards
  5. abort rejects the promise, nothing more

basics

~20 s

A promise is a read-only handle to a result that only its creator can settle, and any number of consumers may hold it, so no consumer can be allowed to cancel it. JavaScript therefore signals cancellation out-of-band with AbortController.

solid answer

~50 s

A promise models a *result*, not the operation producing it. The only code that can settle it is the executor that captured `resolve` and `reject`; a consumer holding the promise has no such power by design. Worse, a promise is a broadcast value — ten places can `await` the same promise, and `.then()` hands each of them a *new* promise — so "cancel" from one consumer would have to steal the result from all the others. TC39 explored adding cancellable promises and withdrew the proposal, and the platform standardised `AbortController`/`AbortSignal` instead: a separate, one-way channel you pass *into* the operation before it starts. `controller.abort(reason)` tells the operation to stop; the operation, if it cooperates, stops its work and rejects its promise with `signal.reason`. The promise still settles exactly once — abort turns a pending promise into a rejected one, it does not make it disappear.

code

javascript · 11 lines
javascript
// One result, several independent consumers — nobody owns it.
let settle;
const source = new Promise((resolve) => { settle = resolve; });

const a = source.then((v) => v + 1);   // a is a NEW promise
const b = source.then((v) => v * 10);  // so is b

console.log(typeof source.cancel); // 'undefined'

settle(5);
Promise.all([a, b]).then(console.log); // [6, 50]

go deeper

for a junior

Be able to say that promises have no cancel method and that AbortController is the standard way to ask an operation to stop. Know that abort surfaces as a rejection, not as a special third state.

for a middle

Explain the mechanics that rule out consumer-side cancellation: only the executor holds resolve/reject, a promise settles once, and many consumers share one result. Then show the signal being passed forward into the operation.

for a senior

Demonstrate the difference between abandoning a result and actually stopping work, including the resources a still-running operation holds after its caller stops waiting. Be ready to say what happens to in-flight side effects.

for a principal

Own the cancellation contract at the API boundary: which entry points accept a signal, whether cancellation propagates to downstream calls, and how you keep an aborted request from leaving half-applied effects across a system.

## What a promise actually is A promise is a *one-shot, write-once container for a future result*, with two capabilities split apart: - The **producer** capability — the `resolve` and `reject` functions — is handed only to the executor passed to `new Promise(...)`. Once the executor returns, nobody else has a reference to them unless it deliberately leaked one. - The **consumer** capability — `.then`, `.catch`, `.finally`, and `await` — is what everybody else gets. It is strictly read-only: you can register interest in the outcome, and that is all. ```js const p = new Promise((resolve, reject) => { // only this scope can decide the outcome }); p.cancel; // undefined — there is no such method ``` That split is not an oversight; it is the invariant that makes promises composable. If any holder could settle a promise, no code could rely on the outcome it was promised. ## Why a cancel() method would be unsound Three properties of promises make consumer-side cancellation incoherent: 1. **Settle-once.** A promise transitions from pending to fulfilled or rejected exactly once and then never changes. A `cancel()` would be a third outcome that every existing consumer would have to be taught to handle. 2. **Many consumers, one result.** A promise can be awaited from arbitrarily many places, and each `.then()` creates a *new* promise downstream. There is no single owner. If one consumer cancelled, the others would lose a result they were legitimately waiting for, through no action of their own. 3. **The result is detached from the work.** By the time you hold a promise, the operation has usually already started, and the promise carries no reference back to it. Cancelling the handle could not reach the work anyway. TC39 did explore a cancellable-promise design, with a distinct "cancelled" state and a cancel token; it was withdrawn rather than shipped, and the ecosystem converged on signalling instead. ## The AbortController answer The standard alternative inverts the direction. Instead of reaching backwards from the result, you pass a cancellation channel *forwards* into the operation before it starts: ```js const controller = new AbortController(); const promise = doWork(input, { signal: controller.signal }); controller.abort(new Error('no longer needed')); // doWork observes the signal, stops, and rejects with that reason ``` The promise's contract is untouched: it still settles once, it just settles as a rejection whose reason is `signal.reason`. Every existing `.catch` and `try/catch` keeps working; there is no new state to learn. ## What abort does not do Cancellation in JavaScript is **cooperative**. `abort()` sets a flag and fires an event — nothing more: - Code that never looks at the signal is unaffected. A `while` loop crunching numbers, or a library function with no `signal` option, runs to completion regardless. - Side effects already performed are not undone. If the operation already wrote a row or sent bytes on the wire, aborting the client-side promise does not reach back and unwind them. - Nothing is preempted mid-statement. Run-to-completion still holds: the current synchronous job finishes before any abort listener that was queued as a task runs — though the `abort` event itself dispatches synchronously inside `abort()`. ## The await limitation This is the sharp edge candidates miss. `await p` resumes only when `p` settles. There is no way for a signal to interrupt an `await` from the outside; if the awaited promise never settles, the async function never resumes, and its local state stays alive. So the abort has to be visible *in the thing you are awaiting*. Either the operation itself accepts the signal and rejects on abort, or you await something composed with a promise that rejects when the signal fires: ```js function rejectOnAbort(signal) { return new Promise((_, reject) => { signal.throwIfAborted(); signal.addEventListener('abort', () => reject(signal.reason), { once: true }); }); } ``` Bailing out this way makes *your* code stop waiting; it does nothing to stop the underlying work, which keeps running unobserved. That distinction — abandoning a result versus genuinely stopping the operation — is exactly what an interviewer is listening for. ## One more consequence Because abort surfaces as a rejection, an aborted operation whose promise has no rejection handler is a plain unhandled rejection. Cancellation being "expected" does not exempt it from being caught.

  • When an abortable operation is cancelled, what happens to the promise it already returned?
    It rejects, with `signal.reason` as the rejection value — the default being a `DOMException` named `AbortError`. The promise still settles exactly once; abort simply decides which way. That means an aborted operation whose promise has no `.catch` produces an unhandled rejection like any other, so cancellation paths need handlers too.
  • Does calling abort() stop code that is already executing?
    No. It sets `signal.aborted`, stores the reason, and fires the `abort` event. Code that does not inspect the signal — a tight computation loop, or a library with no signal support — runs to completion. Effects already applied are not rolled back. Cancellation is a request that the operation must be written to honour.
  • Can a signal interrupt a plain `await somePromise`?
    No. `await` resumes only when that promise settles, and a signal has no way to reach into it. Either the awaited operation accepts the signal and rejects on abort, or you await a composition that includes a promise rejecting on the `abort` event. The second form only stops *your* waiting — the underlying work continues.
  • Why can't you just wrap a promise in a helper that returns a cancellable version of it?
    Because the wrapper can only stop forwarding the result — it cannot reach the operation. You get a promise that rejects early while the original work keeps running, holding its resources and eventually settling into the void. That is abandonment, not cancellation, and it is why the signal has to be passed in before the work starts.

saying these in an interview costs you the question

  • Claims Promise.prototype.cancel exists in modern JavaScript
  • Thinks abort() unwinds the running async function like an exception
  • Believes aborting automatically frees the underlying work
  • Says a signal can interrupt an await mid-wait
  • Confuses dropping the .then handler with cancelling the operation

context