mosaddik.dev
All posts

JavaScript fetch explained: GET, POST and handling errors properly

Mosaddik Billah 4 min read

JavaScript & React

Network cables plugged into a patch panel
Photo by Jordan Harrison on Unsplash
On this page
  1. A simple GET request
  2. The surprise: fetch doesn’t fail on 404 or 500
  3. A reusable helper
  4. Sending data with POST
  5. Query parameters
  6. API keys: browser or server?
  7. Quick checklist

fetch is how JavaScript talks to other servers: loading products from an API, sending a form, calling an AI model from your backend. It works in every modern browser, in Node.js 18 and newer, and in Wix Velo through wix-fetch. The basics take two lines. Using it safely takes a few more, because of one surprise in how it reports errors.

A simple GET request#

JavaScript
const res = await fetch("https://api.example.com/products");
const products = await res.json();

There are two awaits because there are two steps: the first waits for the response to start arriving (status and headers), the second waits for the whole body and reads it as JSON. If await is new to you, see async and await explained.

The surprise: fetch doesn’t fail on 404 or 500#

fetch only rejects when the request can’t be made at all: no internet, a blocked request, a domain that doesn’t exist. If the server answers with 404 Not Found or 500 Server Error, fetch treats that as a successful request and hands you the response.

So this code doesn’t catch a missing product:

JavaScript
try {
  const res = await fetch("https://api.example.com/products/999");
  const product = await res.json(); // may be an error message, not a product
} catch (err) {
  // only network failures land here
}

Check res.ok, which is true for status codes 200 to 299:

JavaScript
const res = await fetch("https://api.example.com/products/999");
if (!res.ok) {
  throw new Error(`Request failed with status ${res.status}`);
}
const product = await res.json();

A reusable helper#

Most projects end up with one small function that does the checks once:

JavaScript
async function getJson(url, options = {}) {
  const res = await fetch(url, {
    ...options,
    signal: options.signal ?? AbortSignal.timeout(10000),
  });

  if (!res.ok) {
    const text = await res.text().catch(() => "");
    throw new Error(`${res.status} ${res.statusText}: ${text.slice(0, 200)}`);
  }

  return res.status === 204 ? null : res.json();
}

It adds three things the plain version lacks:

  • A timeout. fetch has no time limit of its own and can wait a long time on a slow server. AbortSignal.timeout(10000) cancels it after 10 seconds, and the request rejects with a TimeoutError.
  • A useful error message, including the start of what the server said, which is usually the fastest way to see what went wrong.
  • Empty responses. A 204 No Content has no body, so calling res.json() on it would throw.

Sending data with POST#

To send JSON, set the method, a Content-Type header and a body made with JSON.stringify:

JavaScript
const lead = await getJson("https://api.example.com/leads", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "Rahim", email: "rahim@example.com" }),
});

Forgetting JSON.stringify is a common bug: the body is sent as the text [object Object]. Forgetting the header often makes the server ignore the body.

To send a form with a file instead, pass a FormData object as the body and don’t set Content-Type; the browser sets it, including a boundary value the server needs.

Query parameters#

Build them with URLSearchParams so spaces and special characters are encoded correctly:

JavaScript
const params = new URLSearchParams({ q: "desk lamp", category: "home", page: "2" });
const results = await getJson(`https://api.example.com/search?${params}`);
// .../search?q=desk+lamp&category=home&page=2

API keys: browser or server?#

Anything in browser JavaScript can be read by visitors, including headers. If a request needs a secret API key, make it from server code: a Node.js backend, a serverless function, or Velo backend code with the key in the Wix Secrets Manager, as in calling an external API from Wix Velo. Keys made for public use, like a contact form service’s access key, are fine in the browser.

If a browser request to another domain fails with a CORS error, the server hasn’t allowed requests from your site. That has to be fixed on the server, or the call moved to your own backend; it can’t be fixed in the browser code.

Quick checklist#

  • await res.json() only after checking res.ok.
  • Set a timeout with AbortSignal.timeout.
  • JSON.stringify the body and set Content-Type: application/json.
  • Build query strings with URLSearchParams.
  • Keep secret keys out of the browser.

If you need your site connected to another service’s API, I do custom API integration.

Comments