JavaScript Fetch API
Master the Fetch API for making HTTP requests in JavaScript, including GET, POST, error handling, and working with JSON responses.
TL;DR
- 01Use
fetch()to make HTTP requests and await the response. - 02Check
response.ok, since fetch rejects only on network failure. - 03Use async/await with
try/catchfor clean, readable fetch code.
Tips
- 01In Next.js App Router, prefer the built-in extended
fetch, since it supportscacheandrevalidateoptions for data fetching. - 02Always wrap fetch calls in try/catch and check response.ok, so both network failures and HTTP error responses get handled consistently.
- 03Use an
AbortControllerto cancel in-flight requests on unmount, which prevents wasted network calls and stale state updates.
Warnings
- 01Calling
response.json()on an error response that returns HTML, like a 404 page, throws a JSON parse error. - 02Use
res.text()as a safe fallback when the content type of a response is unknown or unconfirmed. - 03Forgetting to set the
Content-Typeheader on a POST request can cause the server to misparse the JSON body.
Basic GET Request
fetch()Returns a Promise that resolves to a Response object, the entry point for every request.
async function getUser(id) {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return response.json();
}response.okTrue for status codes 200-299 — always check it before trusting the response.
response.json()Parses the response body as JSON and returns another Promise.
const data = await response.json();Network-only rejectionfetch only rejects on network-level failures; a 404 or 500 still resolves successfully.
Replaces XMLHttpRequestFetch is the modern, promise-based replacement for the older XMLHttpRequest API.
POST Request with JSON Body
Sending JSONSet method, headers, and body in the options object to send data.
async function createPost(data) {
const response = await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!response.ok) throw new Error('Request failed');
return response.json();
}methodSets the HTTP verb, like 'POST', 'PUT', or 'DELETE'.
headersSets request headers, most commonly Content-Type for JSON bodies.
headers: { 'Content-Type': 'application/json' }bodyHolds the request payload, usually JSON.stringify(data) for JSON APIs.
body: JSON.stringify(data)credentialsControls whether cookies are sent, e.g. 'include' or 'same-origin'.
Error Handling Patterns
Robust try/catchHandle both network errors and HTTP error responses in one function.
async function safeFetch(url, options = {}) {
try {
const res = await fetch(url, options);
if (!res.ok) {
const msg = await res.text();
throw new Error(`${res.status}: ${msg}`);
}
return await res.json();
} catch (err) {
console.error('Fetch failed:', err);
throw err;
}
}Network-level errorsWrap fetch in try/catch to catch network-level errors like being offline or CORS failures.
HTTP error statusCheck response.ok inside the try block to catch HTTP errors (4xx, 5xx).
Reading the error bodyRead res.text() or res.json() on error responses to get the server's error message.
const msg = await res.text();Common Fetch Patterns
Auth tokenSend a bearer token in the Authorization header.
headers: { Authorization: 'Bearer ' + token }Form dataSend a FormData body directly — no Content-Type header needed.
body: new FormData(formEl)Abort a requestCancel an in-flight request with AbortController.
const ac = new AbortController();
fetch(url, { signal: ac.signal });Read plain textRead a non-JSON response body as text.
const text = await response.text();Download a blobRead a binary response body as a Blob.
const blob = await response.blob();Fetch vs Alternatives
fetch()Best for simple requests with no extra dependencies; error handling is more verbose and there are no interceptors.
axiosBest for complex apps needing interceptors and retries; adds an external dependency of about 15 kB gzipped.
SWR / React QueryBest for data fetching in React with built-in caching; framework-specific and needs more setup.
tRPCBest for full-stack TypeScript with end-to-end types; requires a matching server setup.
In Practice
Combines AbortController, try/catch, and a response.ok check to fetch data that gives up after a timeout instead of hanging forever.
- 01The AbortController's signal is passed to fetch, giving the request a way to be cancelled mid-flight.
- 02setTimeout calls controller.abort() if the response doesn't arrive within timeoutMs.
- 03response.ok is checked separately from the try/catch, since fetch only rejects on network failures, not HTTP error codes.
- 04clearTimeout in finally cancels the pending timer once the request settles, whether it succeeded, failed, or timed out.
async function fetchWithTimeout(url, timeoutMs = 5000) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, { signal: controller.signal });
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return await response.json();
} catch (error) {
if (error.name === 'AbortError') {
throw new Error('Request timed out');
}
throw error;
} finally {
clearTimeout(timer);
}
}
fetchWithTimeout('/api/users').then(data => console.log(data));FAQ
fetch() only rejects its promise on network-level failures (e.g., no internet, DNS error). HTTP error status codes like 404 or 500 resolve successfully. You must check response.ok or response.status manually before treating the response as valid.
Set the method to 'POST', add a 'Content-Type': 'application/json' header, and pass JSON.stringify(yourData) as the body. Without the Content-Type header, many servers won't parse the payload correctly.
fetch() is built into modern browsers and Node.js 18+, making it a solid zero-dependency choice for most projects. Axios adds value if you need request/response interceptors, automatic JSON serialization, or broader legacy browser support out of the box.
Wrap your fetch call in a try/catch to handle network failures. Inside the try block, explicitly check response.ok and throw a new Error for HTTP errors. This lets your catch block handle both failure types in one place.
Yes — Next.js App Router extends the native fetch with cache and next.revalidate options. This lets you control caching and incremental static regeneration directly in server components without any additional library.