v154 · network · fetch

Fetch API: forward the abort reason to the Response

You can abort with a reason — any value you like — and the fetch promise rejects with it. But a fetch is not over when the promise settles: the body is still streaming, and reading it rejected with a bare AbortError that told you nothing. Chrome 154 forwards your reason everywhere it was always meant to go.

concepts

  1. Every place it can surface

    Abort one request and catch it at four points — the fetch promise, response.text(), a stream reader, and signal.reason. The table shows which of them carried your reason and which invented an AbortError, which is the whole change.

  2. Timeout or user?

    The case that made this matter. A timeout and a cancel button both abort; only the reason tells them apart, and retrying a timeout while retrying a deliberate cancel are very different behaviours.

  3. Cancelling mid-stream

    A long response read chunk by chunk, cancelled part-way with a structured reason, and routed on it — the shape real code takes when one controller can be aborted for several different causes.

why it shipped

AbortController.abort(reason) has taken an arbitrary reason for years, and it is the only mechanism the platform gives you for saying why something was cancelled. Code that handles user cancellation, request timeouts and navigation teardown through one signal needs that distinction: a timeout is worth retrying, a user pressing cancel is not, and a teardown should not report anything at all.

The reason reached the fetch promise and stopped there. Once you had a Response, every remaining failure — text(), json(), the body stream — rejected with a generic AbortError, so the distinction was lost exactly where a streaming reader spends its time. This closes the gap, and it is a compliance fix rather than a new API: no new surface, the reason just arrives where the standard already said it should.

the API

const controller = new AbortController();
const response = await fetch(url, { signal: controller.signal });

controller.abort({ code: "USER_CANCEL", at: Date.now() });

try {
  await response.text();
} catch (error) {
  // Chrome 154: this is your object.
  // Before:    a generic AbortError DOMException.
  if (error?.code === "USER_CANCEL") hideSpinnerQuietly();
}

references