
The Fetch API for Beginners: Get, Post, JSON, and Errors

fetch() looks simple enough that beginners often underestimate where the awkward parts are.
At a glance, it seems almost too easy:
const response = await fetch('/api/products');That makes it tempting to assume the rest will behave naturally as well. Then the first real‑world problems show up:
- the response is not automatically JSON
- HTTP errors do not throw in the way people expect
- request bodies need converting
- headers matter
None of this makes fetch() bad. It just means it is a lower‑level API than many people first assume.
A Basic Get Request
The simplest use of fetch() is a GET request.
const loadProducts = async (): Promise<void> => { const response = await fetch('/api/products'); const products = await response.json(); console.log(products);};This sends a GET request to the URL and returns a Response object.
That Response object is not the JSON data itself. It is a wrapper around the HTTP response, which means you still need to read the body in the format you expect.
Reading JSON Requires an Extra Step
This is one of the first places beginners go wrong.
fetch() does not automatically parse JSON for you. You need to call:
await response.json();That is a separate asynchronous step because the response body still needs to be consumed and parsed.
If the response body is not valid JSON, that parsing step can fail even if the network request itself technically succeeded.
fetch() does not throw on HTTP errors by default
This is the single most important beginner gotcha.
If the server responds with:
404500403
fetch() does not automatically reject the promise just because the HTTP status is an error.
You still get a Response object. It is up to you to inspect the status.
That is why this matters:
const response = await fetch('/api/products');if (!response.ok) { throw new Error(`Request failed with status ${response.status}`);}response.ok is a convenient boolean that is true for successful status codes in the 200 range.
Without this kind of check, beginners often assume the request "worked" because no exception was thrown, even though the server actually returned an error page or failure payload.
A Safer Get Example
Here is a slightly more realistic pattern:
type Product = { id: number; name: string;};const loadProducts = async (): Promise<Product[]> => { const response = await fetch('/api/products'); if (!response.ok) { throw new Error(`Failed to load products: ${response.status}`); } return response.json() as Promise<Product[]>;};That is already much more robust than assuming every response is valid and successful.
Sending JSON with Post
When sending data, you usually need more than the URL.
For a JSON POST request, you typically provide:
- the
method - the
headers - the
body
For example:
const saveProduct = async (): Promise<void> => { const response = await fetch('/api/products', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ name: 'Notebook', price: 12.99, }), }); if (!response.ok) { throw new Error(`Failed to save product: ${response.status}`); }};The important part here is that the request body is JSON text, not a raw object. That is why JSON.stringify() is needed.
The Request Body is Not Magically Converted
This is the same basic lesson as with localStorage and JSON: objects and JSON text are not the same thing.
If you try to send a plain object directly as the body of a JSON request, you are likely to get the wrong result.
So when the server expects JSON:
- set the
Content‑Typeheader - convert the body with
JSON.stringify()
Those two steps belong together.
Network Failure and HTTP Failure are Different
This is another distinction beginners need early.
A network failure is when the request cannot complete properly at the network layer at all. In that case, fetch() will reject.
An HTTP failure is when the server responds with an error status such as 404 or 500. In that case, fetch() still resolves with a Response; you must check response.ok or response.status.
That means a sensible error‑handling pattern often needs both:
const loadProducts = async (): Promise<void> => { try { const response = await fetch('/api/products'); if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } const products = await response.json(); console.log(products); } catch (error) { console.error('Request failed', error); }};This catches both types of problem more clearly.
fetch() is promise‑based
fetch() returns a promise, which is why await fits it so naturally.
You can use .then() as well:
fetch('/api/products') .then((response) => { if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } return response.json(); }) .then((products) => { console.log(products); }) .catch((error) => { console.error(error); });But for beginners, async / await often makes the request flow easier to read because it looks more like straightforward step‑by‑step code.
A Few Common Mistakes
The same fetch() mistakes appear again and again:
- forgetting to call
response.json() - assuming
fetch()throws automatically for404or500 - sending an object without
JSON.stringify() - forgetting the
Content‑Typeheader for JSON requests - trying to read the same response body twice
Most of those come from the same root cause: treating fetch() as if it were a higher‑level convenience API rather than a fairly direct interface to the request and response flow.
Keep the Shape of the Problem in Mind
The clean mental model is:
- make a request with
fetch() - inspect the response
- decide whether the status is acceptable
- parse the body in the right format
- handle both network and HTTP failure cases deliberately
That is a much stronger foundation than memorising one short example and hoping it covers everything.
Wrapping up
The Fetch API is a simple and powerful browser tool for making HTTP requests, but it expects developers to be explicit about body parsing, request configuration, and error handling. Once you understand that fetch() gives you a Response, not immediate JSON, and that HTTP errors need checking with response.ok, the API becomes much easier to use confidently.
Key Takeaways
fetch()returns aResponse, not parsed JSON.- Use
await response.json()to read a JSON body. - Check
response.okorresponse.statusbecause HTTP errors do not throw automatically. - Use
JSON.stringify()and the correctContent‑Typeheader when sending JSON with POST. - Handle network failure and HTTP failure as related but different problems.
Once you stop expecting fetch() to make every decision for you, it becomes a very straightforward API to work with.
Related Articles

Tagged Template Literals in JavaScript. 
Access Search Parameters in Next.js SSR'd Layout. Access Search Parameters in Next.js SSR'd Layout

Replace Inline Styles in Gatsby with an External CSS File. Replace Inline Styles in Gatsby with an External CSS File

Using the CSS :has Pseudo‑Class. Using the CSS
:hasPseudo‑Class
The CSS overflow Property. The CSS
overflowProperty
Handling API Routes in Next.js: When to Use Server Actions vs. API Routes. Handling API Routes in Next.js: When to Use Server Actions vs. API Routes

Using Vue's Suspense for Asynchronous Components. Using Vue's Suspense for Asynchronous Components

LeetCode: Solving the 'Merge Two Sorted Lists' Problem. LeetCode: Solving the 'Merge Two Sorted Lists' Problem

Track Element Visibility Using Intersection Observer. Track Element Visibility Using Intersection Observer
How to Check an Element Exists with and Without jQuery. How to Check an Element Exists with and Without jQuery

JavaScript Error Handling Patterns. JavaScript Error Handling Patterns

Advanced Techniques for Responsive Web Design. Advanced Techniques for Responsive Web Design