JavaScript Fetch Table
| Piece | What it does | Field note |
|---|---|---|
fetch(url) | Starts a request; resolves with a Response | Rejects ONLY on network failure - see the catch below |
{ method: 'POST' } | Second argument options object | GET/POST/PUT/PATCH/DELETE - default GET, no body allowed on GET |
{ headers: {...} } | Request headers | Content-Type: application/json is set by hand - fetch will not guess |
{ body: JSON.stringify(x) } | Request payload | Strings, Blob, FormData, URLSearchParams - objects must be stringified |
response.ok | true for status 200-299 | THE gotcha: 404 and 500 still resolve - check ok before reading data |
response.status | HTTP status code | Pair with ok for error branches the server actually sent |
response.json() | Parses body as JSON | Returns a promise; rejects with SyntaxError on invalid JSON |
response.text() / .blob() | Body as text or binary | A body can be read exactly once - clones need response.clone() |
response.headers.get(n) | Read a response header | Case-insensitive; CORS exposes only a safe list cross-origin |
AbortController.signal | Cancels an in-flight request | controller.abort() rejects with a TimeoutError-shaped DOMException |
{ credentials: 'include' } | Send cookies cross-origin | Default is same-origin only; server must answer Access-Control-Allow-Credentials |
catch (e) | Network failures and aborts | Never fires for 404/500 - those are successful HTTP exchanges |
if (!response.ok) throw for the status. Bottom line: always set Content-Type on JSON POSTs, always check response.ok, and wire an AbortController for anything a user can navigate away from. Related tools: JS promise table for the mechanics underneath, json formatter to inspect what came back, and URL encoder for the query strings you build.fetch is the browser's HTTP client: one function, an options object, and a Response whose body you read once. The table below is the working twelve - everything a real request needs from the URL to the abort signal - with the field notes that separate working code from code that only works on the happy path.
Bottom line: fetch rejects ONLY when the request could not complete - DNS failure, network down, aborted. A 404 or 500 is a successful HTTP exchange and resolves normally, which is why every production fetch has two error layers: catch for the network, and an if (!response.ok) throw for the status the server actually sent.
The honest part: the second argument does more work than the function itself. Content-Type is set by hand (fetch never guesses that your body is JSON), credentials are same-origin by default (cookies vanish cross-origin until you opt in), and the response body can be read exactly once - response.clone() exists because someone needed the bytes twice.
How to use
- Build the request top-down: URL, method, headers, body - each row is one piece of the options object.
- Read the response in order: check response.ok and status BEFORE parsing; then pick json(), text() or blob() by content.
- Add the safety net last: AbortController for user-navigable requests, and a catch that knows it will only ever see network errors.
Frequently asked questions
Why doesn't fetch throw an error on 404 or 500?
Because the HTTP exchange succeeded - the server was reached, it answered, the response is complete. fetch's promise rejects only for failures BEFORE a response exists: DNS lookup failure, refused connection, network drop, or a manual abort. The status code is data, not an exception, so the standard pattern wraps it explicitly: if (!response.ok) throw new Error('HTTP ' + response.status). After that throw, your existing catch handles both layers uniformly.
How do I send a JSON POST request?
Three pieces, all required: method: 'POST', headers: { 'Content-Type': 'application/json' }, and body: JSON.stringify(data). The header is the step people skip - fetch will happily send the stringified object with no content type, and the server's parser cannot know it is JSON. On the way back, response.json() parses the body and, unlike fetch itself, rejects with a SyntaxError when the body is not valid JSON.
How do I cancel a fetch request?
Create an AbortController, pass controller.signal as the signal option, and call controller.abort() when the request should die - on route change, on a newer search superseding an older one, or on a timeout via setTimeout. The pending fetch rejects with a DOMException named AbortError, which your catch can distinguish by name and ignore. A second use: Signal.timeout(ms) in newer browsers builds the same pattern in one line.
Where did my cookies go in a cross-origin fetch?
Into the default: credentials defaults to same-origin, so cookies travel only when the request targets the origin that set them. Cross-origin needs credentials: 'include' on the request AND Access-Control-Allow-Credentials: true on the server response AND a non-wildcard Access-Control-Allow-Origin - three switches, all required. Missing any one silently drops the cookie, which is why authenticated cross-origin fetches feel randomly broken.