Fetch API & HTTP Requests
debt(d7/e1/b3/t9)
Closest to 'only careful code review or runtime testing' (d7). ESLint can flag bare fetch() calls but detecting the missing response.ok check is a nuanced pattern that requires either a custom ESLint rule or careful code review. TypeScript alone won't catch it since fetch() resolves with a Response object regardless of HTTP status. The detection_hints confirm eslint and typescript as tools, but the specific anti-pattern (not checking response.ok) is not caught by default rules — it takes specialist configuration or review.
Closest to 'one-line patch or single-call swap' (e1). The quick_fix confirms this: add a response.ok check and throw manually after the fetch call, or add AbortController. These are localized, single-site additions at each fetch() call site. No architectural rework required.
Closest to 'localised tax' (b3). The applies_to context is web only, and the burden falls at each fetch() call site. It imposes a recurring but localized discipline (always check response.ok, always handle AbortController) without reshaping the wider codebase architecture.
Closest to 'catastrophic trap — the obvious way is always wrong' (t9). The misconception field states exactly this: developers universally expect a Promise-based HTTP client to reject on 4xx/5xx errors — the same mental model they bring from axios, jQuery AJAX, or most HTTP libraries. fetch() silently resolves instead. The obvious code path (catch block handles errors) misses every HTTP-level error. This is the canonical t9 trap.
Also Known As
TL;DR
Explanation
fetch(url, options) returns a Promise resolving to a Response object. Unlike XMLHttpRequest, fetch doesn't reject on HTTP error status codes (404, 500) — only on network failure. Always check response.ok or response.status: if(!res.ok) throw new Error(res.status). Response body is consumed once via streaming methods: res.json(), res.text(), res.blob(), res.arrayBuffer(). Options: method, headers, body (JSON.stringify(data)), credentials ('include' for cookies), signal (AbortController for cancellation). The AbortController pattern enables timeout handling: const controller = new AbortController(); setTimeout(() => controller.abort(), 5000). In Node.js, fetch is available natively from v18.
Diagram
sequenceDiagram
participant JS as JavaScript
participant FETCH as Fetch API
participant SERVER as Server
JS->>FETCH: fetch url options
Note over FETCH: Returns Promise immediately
FETCH->>SERVER: HTTP Request
SERVER-->>FETCH: HTTP Response
FETCH-->>JS: Response object Promise
JS->>FETCH: response.json()
FETCH-->>JS: Parsed data Promise
Note over JS: Error handling
JS->>FETCH: fetch bad-url
FETCH-->>JS: Rejects on network error
Note over JS: 404 500 do NOT reject<br/>check response.ok
Common Misconception
Why It Matters
Common Mistakes
- Not checking response.ok — fetch resolves (not rejects) for 404 and 500 responses.
- Not handling network errors separately from HTTP errors — both need different user feedback.
- Not setting Content-Type header for POST requests with JSON body.
- Not aborting fetch requests with AbortController when the component unmounts.
Code Examples
// fetch resolves on 404 — error silently ignored:
fetch('/api/user/999')
.then(res => res.json())
.then(data => console.log(data)); // data is {error: 'Not found'} — not thrown!
// Correct:
fetch('/api/user/999')
.then(res => { if (!res.ok) throw new Error(res.status); return res.json(); })
.then(data => console.log(data))
.catch(err => console.error(err));
const res = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Alice' }),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const user = await res.json();