Fetch API & AbortController

Hard100% Free~30 mins#fetch#abortcontroller#abortsignal#http#network#async-cancellation#browser-apis
Key Learning Objectives
✓

Master Fetch API Promise semantics: understand why fetch does NOT reject on HTTP 4xx/5xx status codes.

✓

Validate HTTP response status using response.ok (200-299) and response.status.

✓

Implement request cancellation and timeout bounds using AbortController and AbortSignal.

✓

Handle and discriminate AbortError from general network connection failures.

✓

Implement resilient retry strategies with exponential backoff and stale-request cancellation for UI inputs.

The Interview Problem

What is logged to the console when the following network wrapper function executes with successful, HTTP 404, and timed-out requests using AbortController cancellation?

1async function fetchWithTimeout(mockApi, timeoutMs) {
2 const controller = new AbortController();
3 const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
4
5 try {
6 const response = await mockApi(controller.signal);
7 clearTimeout(timeoutId);
8
9 if (!response.ok) {
10 return 'HTTP-' + response.status;
11 }
12 return await response.json();
13 } catch (err) {
14 clearTimeout(timeoutId);
15 if (err.name === 'AbortError') {
16 return 'REQUEST_ABORTED';
17 }
18 return 'NETWORK_ERROR';
19 }
20}
21
22const okApi = async (signal) => ({
23 ok: true,
24 status: 200,
25 json: async () => 'DATA_OK'
26});
27
28const notFoundApi = async (signal) => ({
29 ok: false,
30 status: 404,
31 json: async () => 'NOT_FOUND'
32});
33
34const hangingApi = (signal) =>
35 new Promise((_, reject) => {
36 signal.addEventListener('abort', () => {
37 const err = new Error('The operation was aborted');
38 err.name = 'AbortError';
39 reject(err);
40 });
41 });
42
43const res1 = await fetchWithTimeout(okApi, 100);
44const res2 = await fetchWithTimeout(notFoundApi, 100);
45const res3 = await fetchWithTimeout(hangingApi, 20);
46
47console.log(res1, res2, res3);
Predict Console Output
Interactive Challenge

Select the option that matches what standard ECMAScript prints to the console:

DATA_OK HTTP-404 REQUEST_ABORTED

DATA_OK NETWORK_ERROR REQUEST_ABORTED

DATA_OK HTTP-404 NETWORK_ERROR

DATA_OK NOT_FOUND REQUEST_ABORTED

V8 Engine Execution Trace
Step 1 of 6 (Line 38)

okApi resolves successfully with ok: true. Clears timer and returns 'DATA_OK'.

Call Stack (Top = Active)
Global Execution Context
fetchWithTimeout(okApi, 100)
Lexical Scope / Bindings
response.ok:true
response.status:200
Microtask Queue (0)
Macrotask Queue (1)
timeout timer (100ms)
Console Stream
> [empty]

Deep Technical Breakdown

Fetch API Mental Model: The #1 Interview Misconception

The most pervasive misconception in frontend interviews is believing fetch() rejects when the server returns a 404 or 500 status. The Fetch Promise rejects ONLY on network failure or if the request was aborted.

javascript
// CORRECT PATTERN FOR ERROR HANDLING IN FETCH
async function safeFetch(url) {
  const res = await fetch(url);
  if (!res.ok) {
    throw new Error(`HTTP Error: ${res.status} ${res.statusText}`);
  }
  return await res.json();
}

Request Cancellation with AbortController

Modern JavaScript applications use AbortController to cancel in-flight network requests:

  1. Timeout Cancellation: Cancel slow requests using AbortSignal.timeout(5000) (supported in modern browsers and Node.js 18+).
  2. Stale Request Cancellation in UI Autocomplete: When a user types into a search input, cancel previous pending queries to prevent race conditions (where an older slow query overwrites a newer fast query):
javascript
let currentController = null;

async function onSearchInput(query) {
  if (currentController) {
    currentController.abort(); // Cancel previous in-flight request
  }
  currentController = new AbortController();
  try {
    const res = await fetch(`/api/search?q=${query}`, { signal: currentController.signal });
    const data = await res.json();
    renderResults(data);
  } catch (err) {
    if (err.name !== 'AbortError') {
      showError(err);
    }
  }
}

Response Body Consumption Rule: bodyUsed

A Fetch Response body is a ReadableStream that can only be read once. Calling await res.json() and subsequently await res.text() throws TypeError: Failed to execute 'text' on 'Response': body stream already read. If you need to read the body multiple times, use const clone = res.clone() beforehand.

Common Traps & Mistakes

Relying on `try/catch` to capture HTTP 404 or 500 responses. Fetch resolves on HTTP error statuses; you must explicitly check `res.ok`.

Attempting to read the response body multiple times (`res.json()` then `res.text()`) without cloning the response stream.

Failing to check `err.name === 'AbortError'`, accidentally displaying user-facing error banners when a request was intentionally cancelled.

Forgetting to clear timeout timers (`clearTimeout(timeoutId)`) when the fetch completes early, leaking timer handles.

FAANG Follow-Up Probes
Probe #1

How does AbortSignal.any([signal1, signal2]) in modern browsers allow cancelling an operation via either a user cancel button or a timeout?

Probe #2

How would you implement exponential backoff with jitter for transient 503 Service Unavailable network errors?

Probe #3

Why does fetch not include cookies in cross-origin requests by default, and what does credentials: 'include' do?