Fetch Is a Promise-Based HTTP Client
fetch is built into browsers and modern Node. You give it a URL, it returns a Promise that resolves to a Response object. That's the whole API at its core:
Two .then calls because there are two asynchronous steps: first the response headers arrive (that's what the first promise resolves with), then the body is read and parsed (response.json() is itself a promise). The body isn't downloaded until you ask for it.
The same flow with async/await reads like normal top-to-bottom code:
Two awaits, two suspension points. Same work, clearer reading order.
The Response Object
What you get back isn't the body — it's a Response object with metadata and methods for reading the body in different shapes:
You can read the body as .json(), .text(), .blob(), .arrayBuffer(), or .formData(). Each returns a promise. You can only read the body once — call .json() twice on the same response and the second call throws.
The Big Gotcha: HTTP Errors Don't Reject
This trips up almost everyone new to fetch. A 404 or 500 response is not a rejection. The promise resolves normally, with response.ok === false. Fetch only rejects when the request itself couldn't complete — DNS failure, no network, CORS block.
That means a naive fetch will happily feed you an error page and crash on .json() later:
The fix is to check response.ok yourself and throw if the server returned an error status:
Get used to writing that if (!response.ok) block. It belongs in every fetch wrapper you write.
Sending a POST Request
GET is the default. For anything else, pass a second argument — an options object:
Three things worth noting:
methoddefaults to"GET". Set it explicitly for POST, PUT, DELETE, PATCH.bodytakes a string (orFormData,Blob, etc.) — fetch won't serialize objects for you.JSON.stringify(...)is on you.- The
Content-Typeheader tells the server how to parse the body. Forget it and most servers will treat the body as plain text.
Headers, Query Strings, and Other Options
Headers are just an object (or a Headers instance). Query strings you build yourself, usually with URLSearchParams:
URLSearchParams handles encoding for you — spaces, ampersands, unicode — so you don't end up with broken URLs when the input has characters that need escaping.
Other options you'll see in real code: credentials: "include" to send cookies cross-origin, cache: "no-store" to bypass the HTTP cache, mode: "cors" (usually the default) to control CORS behavior.
Cancelling a Request with AbortController
Sometimes you want to give up — the user typed a new search query, or the request is taking too long. AbortController is the mechanism:
controller.abort() causes the fetch promise to reject with a DOMException whose name is "AbortError". The finally block clears the timeout so a successful request doesn't leave a dangling timer.
This pattern — fetch plus timeout plus cleanup — is worth wrapping in a helper and reusing everywhere.
A Reusable Wrapper
Put it all together and you get a small helper that handles the boilerplate once:
One place to change headers, one place to handle errors, one place to deal with empty responses. Every non-trivial app ends up with something like this.
Next: Error Handling in Async Code
Fetch is one of the most common places async errors surface, and the response.ok check is just one piece of the puzzle. The next page is about error handling across promises and async/await — where errors go, how to catch them, and the traps that let them slip through silently.
Frequently Asked Questions
How do I use fetch in JavaScript?
Call fetch(url) with the URL you want. It returns a Promise that resolves to a Response object. Call response.json() (also a promise) to parse the body. With async/await: const res = await fetch(url); const data = await res.json();.
How do I send a POST request with fetch?
Pass a second argument with method: 'POST', a headers object (usually 'Content-Type': 'application/json'), and a body — stringify objects with JSON.stringify(...). Fetch won't serialize the body for you.
Why doesn't fetch reject on 404 or 500?
Fetch only rejects on network failures — DNS errors, no connection, CORS blocks. HTTP error statuses are still successful responses as far as the promise is concerned. You have to check response.ok (true for 200–299) or response.status yourself and throw if the server returned an error.
Can I cancel a fetch request?
Yes, with AbortController. Create one, pass its signal to fetch via the options object, and call controller.abort() when you want to cancel. The fetch promise rejects with an AbortError you can handle in catch.