The Fetch API is the standard promise-based interface for making HTTP requests from browser JavaScript and processing their responses. Its central function, fetch(), takes a resource address and optional settings and returns a Promise that resolves to a Response object, which can then be read as text, JSON, binary data or a stream.
Fetch replaced the older XMLHttpRequest as the modern way to load data without reloading the page. It is available in all current browsers, in web workers and service workers, and in server-side runtimes such as Node.js, which makes it the common request primitive across the JavaScript ecosystem.
How a fetch request works
Calling fetch() starts a network request and immediately returns a pending Promise. The Promise resolves as soon as the response headers arrive, not when the whole body has downloaded. Reading the body is a second asynchronous step, using a method such as response.json(), which returns another Promise.
With async/await, the usual pattern is two awaits: one for the response and one for the body. Between them, code should check response.ok, which is true only for status codes 200 to 299, and handle other statuses explicitly.
The API is built from a few interfaces that also appear in service workers and the Cache API. The table summarizes them.
| Interface | Represents | Key members |
|---|---|---|
| fetch() | The request function | Resource, options object, returns a Promise |
| Request | An outgoing request | method, headers, body, signal |
| Response | A received response | status, ok, headers, body methods |
| Headers | A case-insensitive header map | get(), set(), append(), has() |
| AbortController | A cancellation handle | signal, abort() |
Reading the response body
A Response exposes its body through several reader methods, each returning a Promise. The body is a one-time stream: after one method has consumed it, response.bodyUsed becomes true and any further read fails, so the format must be chosen before reading.
json()parses the body as JSON and rejects on malformed input, including an empty body.text()returns a string, useful for HTML fragments, CSV or diagnosing unexpected responses.blob()andarrayBuffer()return binary data for images, files and audio.formData()parses multipart or URL-encoded form bodies.response.bodygives direct access to a ReadableStream for progressive processing.
Streaming lets an application process data as it arrives, for example rendering lines of a large log or tokens of a generated answer, instead of waiting for the full download. Reading the stream chunk by chunk also makes it possible to show download progress when the server sends a Content-Length header.
Request options and their effects
The second argument of fetch() is an options object that configures the request. Several options affect security and caching, so their defaults are worth knowing precisely.
| Option | Default | Purpose |
|---|---|---|
| method | GET | HTTP method such as POST, PUT, DELETE |
| headers | None added | Content type, authorization, custom headers |
| body | None | String, FormData, Blob, URLSearchParams or stream |
| mode | cors | Cross-origin behavior: cors, same-origin, no-cors |
| credentials | same-origin | Whether cookies are sent: omit, same-origin, include |
| cache | default | Interaction with the HTTP cache |
| signal | None | AbortSignal for cancellation or timeout |
When sending JSON, the body must be serialized with JSON.stringify() and the Content-Type header set to application/json. When sending FormData, the header should be left unset, because the browser must add the multipart boundary itself.
Fetch compared with XMLHttpRequest and libraries
Fetch is lower-level and more consistent than its predecessor, but it deliberately omits conveniences that some libraries provide. Knowing the differences prevents subtle bugs during migration.
| Aspect | Fetch API | XMLHttpRequest | Typical HTTP library |
|---|---|---|---|
| Programming model | Promises | Events and callbacks | Promises |
| HTTP error statuses | Resolve normally | Resolve normally | Often reject |
| Upload progress | Not directly available | Available | Usually available |
| Streaming body | Yes, ReadableStream | Limited | Varies |
| Cancellation | AbortController | abort() method | Usually supported |
| Use in service workers | Yes | No | Depends on internals |
The most important behavioral difference is that fetch rejects only on network failure, such as a lost connection, a DNS error or a CORS block. A 404 or 500 response resolves successfully, so code that skips the ok check treats server errors as data.
Cancellation, timeouts and CORS
Fetch has no built-in timeout option. Requests are canceled with an AbortController: its signal is passed in the options, and calling abort() rejects the Promise with an AbortError. Modern browsers also offer AbortSignal.timeout(), which creates a signal that aborts automatically after a set number of milliseconds.
Cross-origin requests follow the same-origin policy and CORS. The server must return appropriate Access-Control-Allow-Origin headers, and requests with custom headers or methods other than simple ones trigger a preflight OPTIONS request. The no-cors mode does not bypass this; it returns an opaque response whose body and status cannot be read.
Common mistakes with the Fetch API
Most fetch bugs come from assumptions carried over from other HTTP tools or from forgetting that the body is read separately.
| Symptom | Cause | Fix |
|---|---|---|
| Error page parsed as data | No response.ok check | Check status and throw on failure |
| Body already used error | Body read twice | Read once or use response.clone() |
| Cookies missing cross-origin | Default credentials setting | Set credentials to include with CORS support |
| Request hangs indefinitely | No timeout | Use an AbortSignal with a timeout |
| Stale results in a search box | Earlier responses arrive late | Abort the previous request on new input |
| Empty opaque response | no-cors mode | Configure CORS on the server instead |
Explained without jargon
The Fetch API is how a web page asks a server for information while you stay on the page. It works like ordering at a counter and getting a receipt. The receipt promises that your order will come, and the page keeps working while it waits.
When the answer arrives, the page reads it and updates what you see. This is how a weather widget refreshes or a search box suggests results without reloading the whole page.
A typical use case
An online store adds live search suggestions. Each time the shopper types, the script calls fetch() with the current query, waits for the response and renders up to eight matching products below the search field.
Early testing reveals two problems. Fast typists see suggestions for an older query flash over newer ones, and a slow server occasionally leaves the spinner running forever. The developer creates a new AbortController for every keystroke and aborts the previous request, adds a 300 millisecond debounce, and combines the signal with a 5 second timeout. Responses with a status other than 200 to 299 now show a short retry message instead of an empty list.
After release, suggestions always match the latest input, server load from search drops noticeably because of the debounce, and no stuck loading states are reported.