← Chrome 154 reference

v154 · network / connectivity · enabled by default

Fetch API: Forward reason from AbortController to fetch Response

Baseline Widely available (parent feature) — this specific fix is engine-dependent

  • Chrome · reason reaches the body from 154
  • Edge · not yet at intent time; Chromium-based
  • Firefox · already compliant
  • Safari · not compliant (Aug 2026)

ChromeStatus maps this change to the abortable-fetch web feature, which is Baseline widely available (low 2019-03-25, high 2021-09-25; queried 2026-10-04). That Baseline covers aborting a fetch at all. The behavior documented on this page — the abort reason propagating into Response body reads — is required by the spec but was missing from Chromium until 154: per the Intent to Ship, “Firefox at least is compliant here but chromium/Edge/Safari are not.” Do not rely on the reason surviving into body reads cross-browser; see the full table.

From Chrome 154, when a fetch() is aborted after its promise has resolved but before the response body has been fully read, the reason you passed to AbortController.abort(reason) is forwarded to the Response body: consumption methods such as response.text() reject with your reason, and the response.body ReadableStream errors with it. Previously those paths produced a generic "AbortError" DOMException and your reason was lost. This is a spec-compliance fix — no new API surface is added.

at a glance

Shipped inChrome 154 (desktop, Android, Android WebView, iOS) — filed under “Enabled by default” in the milestone=154 listing; per-platform milestones from the ChromeStatus API record
Runtime flagForwardReasonToFetchBodyAbort, status stable in Blink’s runtime_enabled_features.json5 (“Flag for rolling out the standardized behavior where fetch calls are rejected with the reason provided to the abort controller”; verified 2026-10-04)
SpecificationFetch Living Standard; the abort-reason forwarding was added by spec commit c9039b9 “Add abort reason support” (Nidhi Jaju, 2022-10-05), a follow-up to whatwg/dom#1027 which added AbortSignal.reason
Tracking bugissues.chromium.org/502133195
Intent to Shipblink-dev thread (2026-08-05), also archived as mail-archive msg17127
Web platform testsfetch/api/abort/general.any.html — the intent names the subtests “response.arrayBuffer() rejects with abort reason if already aborted” and “Stream errors once aborted with abort reason”
ChromeStatus5158507786665984 — Fetch API: Forward reason from AbortController to fetch Response

what changed

Aborting a fetch has two windows. If you abort while the fetch() promise is still pending, the promise rejects with your reason — Chromium already did this (see the earlier abortable-fetch entry). If you abort after the headers arrived — the promise has resolved to a Response, but the body is still streaming — the spec says the reason must also reach everything that is reading that body. That second window is what Chrome 154 fixes:

SurfaceAbort mid-body, Chrome ≤ 153Abort mid-body, Chrome 154+
fetch() promiseAlready settled — unaffected in both. (Aborting before settlement already rejected it with your reason.)
response.arrayBuffer(), .blob(), .bytes(), .formData(), .json(), .text()Rejected with a newly minted generic "AbortError" DOMException; your reason was discardedRejected with your abort reason
response.body (ReadableStream) — pending and subsequent reader.read() callsStream errored with a generic "AbortError"Stream errored with your abort reason

The normative requirement comes from two places in the Fetch Standard. “Abort a fetch() call” (§5.6):

“To abort a fetch() call with a promise, request, responseObject, and an error: Reject promise with error. … If request’s body is non-null and is readable, then cancel request’s body with error. If responseObject is null, then return. Let response be responseObject’s response. If response’s body is non-null and is readable, then error response’s body with error.”

And HTTP-network fetch (§4.7), for the network-layer teardown of the body stream:

“If aborted, then: If fetchParams is aborted, then: Set response’s aborted flag. If stream is readable, then error stream with the result of deserialize a serialized abort reason given fetchParams’s controller’s serialized abort reason and an implementation-defined realm.”

Note the spec also cancels a readable request body with the same error; the Chrome 154 entry describes the Response-side gap specifically, and the request-body side is not separately called out in the sources read.

Source: Fetch Standard §5.6 — abort a fetch() call; Fetch Standard §4.7 — HTTP-network fetch (both quoted verbatim from the Living Standard, fetched 2026-10-04); behavior gap per the ChromeStatus record and Intent to Ship

syntax

No signatures change. The surfaces whose rejection and error values change are the body members of the Response object, defined by the Body interface mixin (IDL verbatim from the Living Standard, fetched 2026-10-04 — textStream() is a newer spec addition quoted as-is, not part of this Chrome 154 change):

interface mixin Body {
  readonly attribute ReadableStream? body;
  readonly attribute boolean bodyUsed;
  [NewObject] Promise<ArrayBuffer> arrayBuffer();
  [NewObject] Promise<Blob> blob();
  [NewObject] Promise<Uint8Array> bytes();
  [NewObject] Promise<FormData> formData();
  [NewObject] Promise<any> json();
  [NewObject] Promise<USVString> text();
  [NewObject] ReadableStream textStream();
};

None of these methods takes arguments; each returns a promise that settles as the body is fully read. The abort reason itself enters the system through AbortController.abort(reason) / AbortSignal, defined in the DOM Standard (IDL verbatim, fetched 2026-10-04):

[Exposed=*]
interface AbortSignal : EventTarget {
  [NewObject] static AbortSignal abort(optional any reason);
  [Exposed=(Window,Worker), NewObject] static AbortSignal timeout([EnforceRange] unsigned long long milliseconds);
  [NewObject] static AbortSignal _any(sequence<AbortSignal> signals);

  readonly attribute boolean aborted;
  readonly attribute any reason;
  undefined throwIfAborted();

  attribute EventHandler onabort;
};

The reason is any JavaScript value. When none is given, the DOM Standard sets the signal’s abort reason “to a new "AbortError" DOMException”, and AbortSignal.timeout() aborts with “a new "TimeoutError" DOMException” — which is what makes timeouts distinguishable from user cancels at the body-read site (see examples). Response objects (and these members) are exposed in Window and Worker contexts.

Source: Fetch Standard §5.3 — Body mixin; DOM Standard §3.2 — Interface AbortSignal

how the reason travels (and when you still get AbortError)

The reason is not passed around by reference. When a fetch controller is aborted, the Fetch Standard stores a structured-serialized copy, and each consumer deserializes it back into its own realm (abort a fetch controller, quoted verbatim):

“Let fallbackError be an "AbortError" DOMException. Set error to fallbackError if it is not given. Let serializedError be StructuredSerialize(error). If that threw an exception, catch it, and let serializedError be StructuredSerialize(fallbackError). Set controller’s serialized abort reason to serializedError.”

And the reverse step (deserialize a serialized abort reason, quoted verbatim):

“To deserialize a serialized abort reason, given null or a Record abortReason and a realm realm: Let fallbackError be an "AbortError" DOMException. Let deserializedError be fallbackError. If abortReason is non-null, then set deserializedError to StructuredDeserialize(abortReason, realm). If that threw an exception or returned undefined, then set deserializedError to fallbackError. Return deserializedError.”

Practical consequences, all directly from those two algorithms:

Source: Fetch Standard §2 — abort a fetch controller; Fetch Standard §2 — deserialize a serialized abort reason; same-realm path per §5.6

examples

Cancelling a download after the headers arrived, and seeing your reason at the body read:

const controller = new AbortController();

const response = await fetch('/api/export.csv', {
  signal: controller.signal,
});
// Headers are in: the fetch() promise has already resolved.
// The body is still streaming.

// Later, the user clicks "cancel":
controller.abort(new DOMException('User cancelled the export', 'AbortError'));
// ...any value is allowed: controller.abort({ why: 'user-cancel' });

try {
  const text = await response.text();
} catch (err) {
  // Chrome 154+ (and Firefox): err IS the reason passed to abort()
  // (a structured clone of it on the cross-realm path).
  // Chrome <= 153: err was a fresh generic "AbortError" DOMException
  // and the reason was lost.
  console.log(err.message); // "User cancelled the export"
}

Telling a timeout apart from a deliberate cancel while reading response.body — the case that makes this fix useful, because AbortSignal.timeout() aborts with a "TimeoutError" DOMException (DOM Standard, quoted above):

const response = await fetch(url, {
  signal: AbortSignal.any([userController.signal, AbortSignal.timeout(10_000)]),
});

const reader = response.body.getReader();
try {
  for (;;) {
    const { done, value } = await reader.read();
    if (done) break;
    render(value);
  }
} catch (err) {
  // Chrome 154+: the stream is errored with the abort reason.
  if (err instanceof DOMException && err.name === 'TimeoutError') {
    offerRetry();        // the 10 s budget ran out
  } else {
    showCancelled(err);  // the user's own abort reason
  }
}
Live example from the Chrome Platform Showcase (route HEAD-checked 200, 2026-10-04): abort one request and inspect the reason at the fetch promise, response.text(), a stream reader, and signal.reason. Sibling concept demos (both 200): timeout vs. user cancel and cancelling mid-stream.Source: chrome-platform-showcase
Source: behavior per the Fetch Standard and Intent to Ship; TimeoutError per the DOM Standard; snippets gendn-derived (labelled accordingly), exercised live by the embedded showcase demo

browser compatibility

BCD has no entry for this behavior change yet: BCD api/Response.json contains no abort-reason subfeature or version note (raw file checked 2026-10-04 — zero matches for “abort”). The table below is therefore interim, built from the ChromeStatus record, the Intent to Ship, and BCD for the prerequisite AbortSignal.reason:

BrowserAbort reason forwarded to Response body readsNotes
Chrome154 (interim: ChromeStatus, not yet BCD)Desktop, Android, Android WebView and iOS all 154 per the ChromeStatus API record; runtime flag ForwardReasonToFetchBodyAbort is stable
EdgeUnknown“chromium/Edge… are not” compliant per the Intent (2026-08); as a Chromium browser it is expected to inherit the fix at its 154-based release, but no source read here records an Edge version
FirefoxAlready compliant; version unknown“Firefox at least is compliant here” (Intent); vendor view “Shipped/Shipping” in the ChromeStatus record. The first compliant Firefox version is not recorded in the sources read
SafariNot compliant (as of 2026-08)Per the Intent; the ChromeStatus record links the WebKit standards-position entry WebKit/standards-positions#711 as “Closed Without a Position”
Prerequisite — AbortSignal.reason itself, per BCD api/AbortSignal.json (verified 2026-10-04): Chrome 98, Firefox 97, Safari 15.4, Edge mirrors Chrome.

Cross-engine status of the two relevant subtests (“response.arrayBuffer() rejects with abort reason if already aborted”, “Stream errors once aborted with abort reason”) is tracked on wpt.fyi; per-browser subtest results were not individually verified here. Write code that treats the reason as a progressive enhancement: always handle a plain "AbortError" DOMException as the baseline outcome.

Source: ChromeStatus API record; Intent to Ship; BCD api/AbortSignal.json; BCD api/Response.json (no entry); webstatus.dev abortable-fetch

security and privacy

No new capability is exposed: the abort reason is a value the page itself supplied to abort(), delivered back to the same page’s own Response object. The structured-serialize/deserialize round trip (quoted in how the reason travels) is the trust boundary: the reason crosses between realms and off the network thread only as a structured clone, never as a live object reference, and anything that cannot be cloned is replaced by the neutral "AbortError" fallback rather than leaked or thrown across the boundary. Nothing about the reason is sent on the network, and the server cannot observe it — aborting still simply tears down the connection (for HTTP/2, the spec transmits an RST_STREAM frame). The change is observable only to script that already holds the Response.

Source: Fetch Standard §2 — abort a fetch controller; Fetch Standard §4.7 — HTTP-network fetch

specifications

Source: both Living Standards fetched and quoted 2026-10-04; forwarding added by whatwg/fetch commit c9039b9

see also