Skip to content
Innovialabs

How to Use the Fetch API: Requests, Errors, Timeouts, Abort

Practice4 min

In short

fetch only rejects on network failure, so you must check response.ok yourself, and timeouts and cancellation are done with AbortController or AbortSignal.timeout.

The essentials

The Fetch API is the built-in way to send HTTP requests in browsers and Node.js. It returns a promise that resolves to a Response object, from which you read the status, headers and body.

Three things to know up front:

  • fetch does not throw on 404 or 500, only on network failure or abort;
  • the body is read separately: response.json(), response.text(), response.blob();
  • there is no built-in timeout; you set one with AbortController.

A GET request

const res = await fetch('/api/products?category=phones');
if (!res.ok) {
  throw new Error(`HTTP ${res.status}`);
}
const products = await res.json();

Build query strings with URLSearchParams so you do not have to escape values by hand:

const params = new URLSearchParams({ q: 'phone case', page: '2' });
const res = await fetch(`/api/search?${params}`);

POST with JSON

const res = await fetch('/api/orders', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ productId: 42, quantity: 1 }),
});

Without the Content-Type: application/json header, many servers will not parse the body.

POST with FormData

For files and regular HTML forms, use FormData:

const form = document.querySelector('form');
const res = await fetch('/api/upload', {
  method: 'POST',
  body: new FormData(form),
});

Do not set Content-Type yourself. The browser sets multipart/form-data with the correct boundary. If you set the header manually, the server cannot parse the body.

Why 404 and 500 are not errors

From fetch's point of view, the server responded, so the request succeeded. Checking the status is your job. It is convenient to put this in one function:

async function request(url, options = {}) {
  const res = await fetch(url, options);
  if (!res.ok) {
    let details = null;
    try {
      details = await res.json();
    } catch {
      // body is not JSON, keep null
    }
    const error = new Error(`HTTP ${res.status}`);
    error.status = res.status;
    error.details = details;
    throw error;
  }
  return res.status === 204 ? null : res.json();
}

Now your code can tell cases apart:

  • TypeError: network down, CORS block, invalid URL;
  • AbortError or TimeoutError: the request was cancelled or timed out;
  • an error with a status field: the server answered with 4xx or 5xx.

Timeouts and cancellation

The shortest option is AbortSignal.timeout():

const res = await fetch('/api/report', {
  signal: AbortSignal.timeout(5000),
});

When you need to cancel manually (the user left the page or typed a new search query), use AbortController:

let controller;

async function search(query) {
  controller?.abort();
  controller = new AbortController();
  try {
    const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
      signal: controller.signal,
    });
    return await res.json();
  } catch (err) {
    if (err.name === 'AbortError') return null;
    throw err;
  }
}

This way stale responses never overwrite fresh search results.

Headers and cookies

  • Pass an auth token in a header: headers: { Authorization: 'Bearer ' + token }.
  • The credentials option controls cookies: the default is same-origin; cross-origin requests need include.
  • With credentials: 'include', the server must allow it in its CORS headers, or the browser blocks the response.

When to reach for axios

Taskfetchaxios
No dependenciesYesNo, a separate library
Error on 4xx/5xxYour own checkBuilt in
Interceptors (tokens, logging)Write a wrapperBuilt in
Upload progressAwkwardSupported
Automatic JSONCall res.json()Automatic

If a project makes a handful of requests, fetch plus a small wrapper is enough. If there are many requests, shared token refresh logic and upload progress, axios saves time.

Common mistakes

  • Skipping the res.ok check and trying to parse an HTML error page as JSON.
  • Setting Content-Type for FormData.
  • Forgetting await before res.json().
  • Not handling AbortError and showing users an error on a normal cancellation.

FAQ

Because the server responded, so at the network level the request succeeded. Check res.ok or res.status and throw the error yourself.

We can help with this

If you'd rather get the result than figure it all out yourself — this is what we do every day.

Discuss a project

Read next

Have an idea or a task? Tell us about it

We'll reply within one business day and propose a solution. The consultation is free.