How to Use the Fetch API: Requests, Errors, Timeouts, Abort
Practice4 minInnovialabs editorial team
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
statusfield: 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 needinclude. - With
credentials: 'include', the server must allow it in its CORS headers, or the browser blocks the response.
When to reach for axios
| Task | fetch | axios |
|---|---|---|
| No dependencies | Yes | No, a separate library |
| Error on 4xx/5xx | Your own check | Built in |
| Interceptors (tokens, logging) | Write a wrapper | Built in |
| Upload progress | Awkward | Supported |
| Automatic JSON | Call 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.okcheck and trying to parse an HTML error page as JSON. - Setting
Content-Typefor FormData. - Forgetting
awaitbeforeres.json(). - Not handling AbortError and showing users an error on a normal cancellation.