Difficulty: Intermediate
The Fetch API is the modern standard for making HTTP requests in JavaScript, replacing the older XMLHttpRequest. It provides a clean, promise-based interface that integrates naturally with async/await. Fetch is available in all modern browsers and in Node.js 18+, making it the universal choice for network requests.
The simplest fetch call is fetch(url), which makes a GET request and returns a Promise that resolves to a Response object. The Response object contains metadata about the response (status, headers, ok) and methods to read the body in different formats: .json() for JSON data, .text() for plain text, .blob() for binary data like images, .formData() for form data, and .arrayBuffer() for raw bytes. Each body method also returns a Promise and can only be called once - the body is a stream that is consumed on read.
For requests other than GET, you pass an options object as the second argument to fetch. This object can include method (GET, POST, PUT, PATCH, DELETE), headers (an object or Headers instance), body (the request payload - typically JSON.stringify(data) for JSON APIs), mode (cors, no-cors, same-origin), credentials (omit, same-origin, include for cookies), and signal (for request cancellation). When sending JSON, you must set the Content-Type header to 'application/json' - fetch does not do this automatically.
Error handling with fetch has an important nuance: fetch only rejects the promise on network errors (DNS failure, no internet, CORS blocked). HTTP error responses like 404 or 500 are considered successful fetches - the promise resolves normally. You must check response.ok (true for status 200-299) or response.status manually to detect HTTP errors. This is different from libraries like axios that throw on non-2xx status codes.
AbortController provides a mechanism to cancel fetch requests. You create a controller with new AbortController(), pass controller.signal to the fetch options, and call controller.abort() to cancel. The fetch promise rejects with an AbortError. This is essential for cleanup in React components (preventing state updates after unmount), implementing request timeouts, and canceling stale search-as-you-type requests. AbortController can cancel multiple fetches simultaneously by passing the same signal to each.
Comparing fetch to alternatives: XMLHttpRequest is the legacy API with an event-based interface and upload progress support. Axios is a popular library that adds automatic JSON parsing, request/response interceptors, automatic error throwing on non-2xx responses, and cancellation (now using AbortController too). For most use cases, fetch with a small wrapper function provides everything you need without a dependency.
// Basic GET request
async function getUser(id) {
const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
// Check for HTTP errors (fetch doesn't throw on 4xx/5xx)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const user = await response.json();
return user;
}
try {
const user = await getUser(1);
console.log(user.name);
console.log(user.email);
} catch (err) {
console.error('Failed:', err.message);
}
// Response metadata
const res = await fetch('https://jsonplaceholder.typicode.com/posts');
console.log('Status:', res.status);
console.log('OK:', res.ok);
console.log('Content-Type:', res.headers.get('content-type'));
const posts = await res.json();
console.log('Posts count:', posts.length);
fetch returns a Response object. Always check response.ok before reading the body, since fetch does not throw on HTTP errors. The .json() method parses the response body as JSON and returns a promise.
// POST - create a resource
async function createPost(data) {
const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer my-token'
},
body: JSON.stringify(data)
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}
const newPost = await createPost({
title: 'Hello',
body: 'World',
userId: 1
});
console.log('Created:', newPost);
// PUT - replace a resource
async function updatePost(id, data) {
const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
return response.json();
}
// DELETE - remove a resource
async function deletePost(id) {
const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`, {
method: 'DELETE'
});
console.log('Deleted:', response.ok);
}
await deletePost(1);
For POST/PUT, set the Content-Type header and pass the data as a JSON string in the body. DELETE requests typically do not need a body. Always check response.ok for error detection.
// Comprehensive error handling
async function safeFetch(url, options = {}) {
try {
const response = await fetch(url, options);
// HTTP errors (4xx, 5xx) - fetch does NOT throw for these
if (!response.ok) {
const errorBody = await response.text();
throw new Error(`HTTP ${response.status}: ${errorBody}`);
}
return await response.json();
} catch (error) {
if (error.name === 'AbortError') {
console.log('Request was cancelled');
} else if (error.name === 'TypeError') {
// Network error (no internet, DNS failure, CORS)
console.error('Network error:', error.message);
} else {
// HTTP error or JSON parse error
console.error('Request failed:', error.message);
}
throw error;
}
}
// Usage
try {
const data = await safeFetch('https://jsonplaceholder.typicode.com/posts/1');
console.log(data.title);
} catch (e) {
// Already logged above
}
// Fetching non-existent resource (404)
try {
await safeFetch('https://jsonplaceholder.typicode.com/posts/99999');
} catch (e) {
console.log('Caught:', e.message);
}
This wrapper distinguishes between network errors (TypeError - no connection), cancellation (AbortError), and HTTP errors (non-2xx status codes). Fetch only throws on network failures, so HTTP errors must be detected manually via response.ok.
// Cancel a single request
async function fetchWithTimeout(url, timeoutMs = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, { signal: controller.signal });
clearTimeout(timeoutId);
return await response.json();
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error(`Request timed out after ${timeoutMs}ms`);
}
throw error;
}
}
// Cancel stale requests (search-as-you-type)
let currentController = null;
async function searchUsers(query) {
// Cancel the previous request if still pending
if (currentController) {
currentController.abort();
}
currentController = new AbortController();
try {
const response = await fetch(
`https://jsonplaceholder.typicode.com/users?q=${query}`,
{ signal: currentController.signal }
);
const users = await response.json();
console.log(`Results for "${query}":`, users.length);
return users;
} catch (error) {
if (error.name === 'AbortError') {
console.log(`Search for "${query}" was cancelled`);
return [];
}
throw error;
}
}
// Simulate rapid typing
await searchUsers('al'); // cancelled
await searchUsers('ali'); // cancelled
await searchUsers('alice'); // this one completes
AbortController lets you cancel in-flight fetch requests. This is essential for search-as-you-type (cancel stale requests), request timeouts, and React cleanup (cancel requests when a component unmounts).
fetch, Response, Headers, JSON, AbortController, Error Handling, HTTP Methods