Menu

JavaScript Fetch API: Requests, JSON, and Error Handling

How to use the Fetch API in JavaScript — GET and POST requests, parsing JSON, handling errors properly, and aborting slow requests.

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:

index.js
Output
Click Run to see the output here.

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:

index.js
Output
Click Run to see the output here.

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:

index.js
Output
Click Run to see the output here.

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:

index.js
Output
Click Run to see the output here.

The fix is to check response.ok yourself and throw if the server returned an error status:

index.js
Output
Click Run to see the output here.

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:

index.js
Output
Click Run to see the output here.

Three things worth noting:

  • method defaults to "GET". Set it explicitly for POST, PUT, DELETE, PATCH.
  • body takes a string (or FormData, Blob, etc.) — fetch won't serialize objects for you. JSON.stringify(...) is on you.
  • The Content-Type header 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:

index.js
Output
Click Run to see the output here.

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:

index.js
Output
Click Run to see the output here.

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:

index.js
Output
Click Run to see the output here.

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.

Learn to code with Coddy

GET STARTED