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

Hero image for The Fetch API for Beginners: Get, Post, JSON, and Errors. Image by Gabriel Soto.
Hero image for 'The Fetch API for Beginners: Get, Post, JSON, and Errors.' Image by Gabriel Soto.

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 realworld 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 lowerlevel 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:

  • 404
  • 500
  • 403

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 ContentType header
  • 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 errorhandling 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 promisebased

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 stepbystep code.


A Few Common Mistakes

The same fetch() mistakes appear again and again:

  • forgetting to call response.json()
  • assuming fetch() throws automatically for 404 or 500
  • sending an object without JSON.stringify()
  • forgetting the ContentType header 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 higherlevel 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:

  1. make a request with fetch()
  2. inspect the response
  3. decide whether the status is acceptable
  4. parse the body in the right format
  5. 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 a Response, not parsed JSON.
  • Use await response.json() to read a JSON body.
  • Check response.ok or response.status because HTTP errors do not throw automatically.
  • Use JSON.stringify() and the correct ContentType header 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.


Categories:

  1. Development
  2. Front‑End Development
  3. Guides
  4. JavaScript