Introduction
The Fetch API is the modern standard for making HTTP requests in JavaScript. It replaces XMLHttpRequest with a cleaner, Promise-based interface. Combined with async/await, it makes network code readable and maintainable.
Basic GET Request
// Promise chain
fetch('https://api.github.com/users/github')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return response.json();
})
.then(data => console.log(data))
.catch(error => console.error('Fetch failed:', error));
// async/await (preferred)
async function getUser(username) {
const response = await fetch(`https://api.github.com/users/${username}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
}
const user = await getUser('github');
console.log(user.name);
Important: fetch() only rejects on network failure. HTTP errors (404, 500) resolve successfully โ you must check response.ok.
POST Request with JSON
async function createUser(userData) {
const response = await fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify(userData),
});
if (!response.ok) {
const error = await response.json().catch(() => ({ message: response.statusText }));
throw new Error(error.message || `HTTP ${response.status}`);
}
return response.json();
}
const newUser = await createUser({
name: 'Alice',
email: '[email protected]',
});
Request Options Reference
const response = await fetch(url, {
method: 'POST', // GET, POST, PUT, PATCH, DELETE
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token123',
'X-Custom-Header': 'value',
},
body: JSON.stringify(data), // string, FormData, Blob, URLSearchParams
mode: 'cors', // cors, no-cors, same-origin
credentials: 'include', // omit, same-origin, include
cache: 'no-cache', // default, no-store, reload, no-cache, force-cache
redirect: 'follow', // follow, error, manual
signal: controller.signal, // AbortController signal
});
Response Methods
const response = await fetch(url);
// Parse response body (choose one)
const json = await response.json(); // parse as JSON
const text = await response.text(); // parse as string
const blob = await response.blob(); // parse as Blob (files, images)
const buffer = await response.arrayBuffer(); // parse as ArrayBuffer
const form = await response.formData(); // parse as FormData
// Response metadata
response.ok // true if status 200-299
response.status // HTTP status code (200, 404, 500...)
response.statusText // "OK", "Not Found", "Internal Server Error"
response.headers // Headers object
response.url // final URL (after redirects)
response.redirected // true if redirected
Error Handling Patterns
Comprehensive Error Handler
class FetchError extends Error {
constructor(message, status, data) {
super(message);
this.name = 'FetchError';
this.status = status;
this.data = data;
}
}
async function apiFetch(url, options = {}) {
let response;
try {
response = await fetch(url, {
headers: { 'Content-Type': 'application/json', ...options.headers },
...options,
});
} catch (err) {
// Network error (no internet, DNS failure, CORS)
throw new FetchError(`Network error: ${err.message}`, 0, null);
}
if (!response.ok) {
let errorData = null;
try {
errorData = await response.json();
} catch {
errorData = { message: response.statusText };
}
throw new FetchError(
errorData.message || `HTTP ${response.status}`,
response.status,
errorData
);
}
// Handle empty responses (204 No Content)
if (response.status === 204) return null;
return response.json();
}
// Usage
try {
const user = await apiFetch('/api/users/42');
} catch (err) {
if (err instanceof FetchError) {
switch (err.status) {
case 0: console.error('No internet connection'); break;
case 401: redirectToLogin(); break;
case 403: showPermissionError(); break;
case 404: showNotFound(); break;
case 422: showValidationErrors(err.data); break;
default: console.error('API error:', err.message);
}
}
}
Authentication
Bearer Token
const token = localStorage.getItem('authToken');
const response = await fetch('/api/profile', {
headers: {
'Authorization': `Bearer ${token}`,
},
});
Automatic Token Refresh
async function fetchWithAuth(url, options = {}) {
let token = getAccessToken();
let response = await fetch(url, {
...options,
headers: { ...options.headers, 'Authorization': `Bearer ${token}` },
});
// Token expired โ refresh and retry
if (response.status === 401) {
token = await refreshAccessToken();
response = await fetch(url, {
...options,
headers: { ...options.headers, 'Authorization': `Bearer ${token}` },
});
}
return response;
}
Cookies (Cross-Origin)
// Include cookies in cross-origin requests
const response = await fetch('https://api.example.com/data', {
credentials: 'include', // sends cookies
});
Timeouts with AbortController
Fetch has no built-in timeout. Use AbortController:
async function fetchWithTimeout(url, options = {}, timeoutMs = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, {
...options,
signal: controller.signal,
});
clearTimeout(timeoutId);
return response;
} catch (err) {
clearTimeout(timeoutId);
if (err.name === 'AbortError') {
throw new Error(`Request timed out after ${timeoutMs}ms`);
}
throw err;
}
}
// Usage
const response = await fetchWithTimeout('/api/slow-endpoint', {}, 3000);
Cancel a Request
const controller = new AbortController();
// Start request
const fetchPromise = fetch('/api/large-data', { signal: controller.signal });
// Cancel it (e.g., user navigated away)
controller.abort();
try {
await fetchPromise;
} catch (err) {
if (err.name === 'AbortError') {
console.log('Request cancelled');
}
}
Uploading Files
// Single file
async function uploadFile(file) {
const formData = new FormData();
formData.append('file', file);
formData.append('description', 'My upload');
const response = await fetch('/api/upload', {
method: 'POST',
body: formData,
// Don't set Content-Type โ browser sets it with boundary
});
return response.json();
}
// With progress tracking (XMLHttpRequest needed for upload progress)
// Fetch doesn't support upload progress natively
Parallel Requests
// Fetch multiple resources in parallel
const [users, posts, comments] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/comments').then(r => r.json()),
]);
// First to resolve wins
const result = await Promise.race([
fetch('/api/primary').then(r => r.json()),
fetch('/api/backup').then(r => r.json()),
]);
// All settle (don't fail on individual errors)
const results = await Promise.allSettled([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
]);
results.forEach((result, i) => {
if (result.status === 'fulfilled') {
console.log(`Request ${i} succeeded:`, result.value);
} else {
console.error(`Request ${i} failed:`, result.reason);
}
});
Retry Logic
async function fetchWithRetry(url, options = {}, retries = 3, delay = 1000) {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
const response = await fetch(url, options);
// Don't retry client errors (4xx)
if (response.status >= 400 && response.status < 500) {
return response;
}
if (response.ok) return response;
// Server error โ retry
if (attempt === retries) return response;
} catch (err) {
if (attempt === retries) throw err;
}
// Exponential backoff
await new Promise(r => setTimeout(r, delay * attempt));
console.log(`Retry ${attempt}/${retries} for ${url}`);
}
}
Building a Simple API Client
class ApiClient {
constructor(baseURL, defaultHeaders = {}) {
this.baseURL = baseURL;
this.defaultHeaders = defaultHeaders;
}
async request(path, options = {}) {
const url = `${this.baseURL}${path}`;
const response = await fetch(url, {
...options,
headers: { ...this.defaultHeaders, ...options.headers },
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw Object.assign(new Error(error.message || response.statusText), {
status: response.status,
data: error,
});
}
if (response.status === 204) return null;
return response.json();
}
get(path, headers) { return this.request(path, { method: 'GET', headers }); }
post(path, body, headers) { return this.request(path, { method: 'POST', body: JSON.stringify(body), headers }); }
put(path, body, headers) { return this.request(path, { method: 'PUT', body: JSON.stringify(body), headers }); }
patch(path, body, headers) { return this.request(path, { method: 'PATCH', body: JSON.stringify(body), headers }); }
delete(path, headers) { return this.request(path, { method: 'DELETE', headers }); }
}
// Usage
const api = new ApiClient('https://api.example.com', {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
});
const user = await api.get('/users/42');
const newPost = await api.post('/posts', { title: 'Hello', body: 'World' });
await api.delete('/posts/1');
Comments