Quick answer: fetch() makes HTTP requests from the browser and returns a promise for a Response. Call res.json() to parse JSON, check res.ok before trusting the result, and pass a second argument with method, headers and a stringified body for anything other than a GET. Remember to attach an abort signal so a slow request can be cancelled.The JavaScript fetch API is the browser’s own way to make that request, and every wrapper you have seen is a thin layer over it.The sections below walk the JavaScript fetch API through a real request: GET, sending data, uploading a file, cancellation, then the response handling where most JavaScript fetch bugs come from.

A GET request

async function getUsers() {
  const res = await fetch("https://api.example.com/users");
  if (!res.ok) throw new Error(`Request failed: ${res.status}`);
  return res.json();
}

const users = await getUsers();

The two-step dance — await fetch, then await res.json() — is because the first await resolves when the headers arrive and the second reads the body stream. That also means a 500 response reaches your code without throwing; you must check res.ok.

Sending data

async function createUser(payload) {
  const res = await fetch("/api/users", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(payload),
  });
  if (!res.ok) throw new Error(`Create failed: ${res.status}`);
  return res.json();
}

For PUT, PATCH and DELETE the shape is identical; only method changes. The Content-Type: application/json header tells the server how to parse the body, and it is easy to forget — a request without it often arrives at the server as an unreadable blob.

Uploading a file

For files or a mix of fields, use FormData and let the browser set the content type and boundary for you.

const form = document.querySelector("form");
const data = new FormData(form);      // collects every named field

await fetch("/api/upload", { method: "POST", body: data });

Do not set Content-Type manually with FormData; the boundary string must match the body, and the browser only generates it when it sets the header itself.

JavaScript fetch timeouts and cancellation

fetch has no timeout of its own. Use an AbortController to cancel a request that takes too long, or to cancel a previous search when the user types again.

const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 8000);

try {
  const res = await fetch("/api/search?q=angular", { signal: controller.signal });
  return await res.json();
} catch (err) {
  if (err.name === "AbortError") console.log("Timed out");
  else console.error(err);
} finally {
  clearTimeout(timer);
}

That pattern is essential in search boxes. Keep the controller for the current query, abort it when a new keystroke arrives, and the UI can never show results from a stale request.

JavaScript fetch errors and response helpers

MethodReturns
res.json()Parsed JSON
res.text()The body as a string
res.blob()Binary data, for images or files
res.okTrue for a 2xx status
res.statusThe numeric status code
async function safeFetch(url, options) {
  try {
    const res = await fetch(url, options);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return await res.json();
  } catch (err) {
    return { error: err.message };
  }
}

A single wrapper like this keeps every call site free of duplicated status checks, and it is the natural place to add a retry later. Next: modules, which split code across files. To build against real APIs with a mentor, Ampersand Academy teaches web development one-to-one.

Frequently asked questions

Why does fetch not throw on an HTTP error?

fetch only rejects when the request itself fails, such as a network error. An HTTP status such as 404 or 500 still resolves, so you must check response.ok and throw your own error.

How do I send JSON with fetch?

Use method POST, set the Content-Type header to application/json, and pass JSON.stringify of your object as the body.

How do I cancel a fetch request?

Create an AbortController, pass its signal in the fetch options, and call controller.abort() when you want to cancel, such as on a new search keystroke or a timeout.

Should I set Content-Type for FormData?

No. Let the browser set it, because it must include a boundary string that matches the body. Setting the header yourself produces an unreadable request.

How do I handle multiple fetch requests at once?

Build an array of promises and await Promise.all or Promise.allSettled on it, so the requests run in parallel instead of one after another.

Last updated on · Written by