What is the Fetch API?
The Fetch API provides a modern, promise-based interface for making HTTP requests. It's the replacement for XMLHttpRequest (XHR).
🚀 Basic Usage
Simple GET Request
// Basic fetch (defaults to GET)
fetch('https://api.example.com/users')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
// With async/await
async function getUsers() {
try {
let response = await fetch('https://api.example.com/users');
let data = await response.json();
console.log(data);
} catch (error) {
console.error('Error:', error);
}
}
// Fetch returns a promise
let promise = fetch('/api/data');
console.log(promise); // Promise { }
Response Object
async function example() {
let response = await fetch('/api/users');
// Response properties
console.log(response.ok); // true if 200-299
console.log(response.status); // 200, 404, 500, etc.
console.log(response.statusText); // 'OK', 'Not Found', etc.
console.log(response.url); // Final URL (after redirects)
console.log(response.headers); // Headers object
console.log(response.redirected); // true if redirected
console.log(response.type); // 'basic', 'cors', 'opaque'
// Check if successful
if (response.ok) {
let data = await response.json();
console.log(data);
} else {
console.error('HTTP error:', response.status);
}
}
📄 Reading Response Body
// JSON data (most common)
let response = await fetch('/api/users');
let data = await response.json();
console.log(data);
// Plain text
let response = await fetch('/api/text');
let text = await response.text();
console.log(text);
// Blob (images, files)
let response = await fetch('/images/photo.jpg');
let blob = await response.blob();
let img = document.createElement('img');
img.src = URL.createObjectURL(blob);
// ArrayBuffer (binary data)
let response = await fetch('/api/binary');
let buffer = await response.arrayBuffer();
// FormData
let response = await fetch('/api/form');
let formData = await response.formData();
// Important: Body can only be read once
let response = await fetch('/api/users');
let data1 = await response.json();
let data2 = await response.json(); // Error: body already consumed
// Clone response if needed
let response = await fetch('/api/users');
let clone = response.clone();
let data1 = await response.json();
let data2 = await clone.json(); // Works
⚙️ Request Options
HTTP Methods
// GET (default)
fetch('/api/users');
// POST
fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: 'John',
email: 'john@example.com'
})
});
// PUT
fetch('/api/users/1', {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: 'John Doe',
email: 'john@example.com'
})
});
// PATCH
fetch('/api/users/1', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: 'newemail@example.com'
})
});
// DELETE
fetch('/api/users/1', {
method: 'DELETE'
});
Headers
// Set headers
fetch('/api/data', {
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token123',
'X-Custom-Header': 'value'
}
});
// Headers object
let headers = new Headers();
headers.append('Content-Type', 'application/json');
headers.append('Authorization', 'Bearer token123');
fetch('/api/data', { headers });
// Read response headers
let response = await fetch('/api/data');
console.log(response.headers.get('Content-Type'));
console.log(response.headers.get('Date'));
// Iterate headers
for (let [key, value] of response.headers) {
console.log(`${key}: ${value}`);
}
// Check if header exists
if (response.headers.has('Content-Type')) {
console.log('Has Content-Type header');
}
Request Body
// JSON body
fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'John', age: 30 })
});
// FormData (multipart/form-data)
let formData = new FormData();
formData.append('name', 'John');
formData.append('avatar', fileInput.files[0]);
fetch('/api/upload', {
method: 'POST',
body: formData // Content-Type set automatically
});
// URLSearchParams (application/x-www-form-urlencoded)
let params = new URLSearchParams();
params.append('name', 'John');
params.append('age', '30');
fetch('/api/form', {
method: 'POST',
body: params
});
// Plain text
fetch('/api/text', {
method: 'POST',
headers: { 'Content-Type': 'text/plain' },
body: 'Hello, server!'
});
// Blob
let blob = new Blob(['Hello'], { type: 'text/plain' });
fetch('/api/blob', {
method: 'POST',
body: blob
});
Other Options
fetch('/api/data', {
method: 'GET',
// Credentials (cookies)
credentials: 'include', // Send cookies cross-origin
// credentials: 'same-origin', // Only same origin (default)
// credentials: 'omit', // Never send cookies
// Cache
cache: 'default', // Use browser cache
// cache: 'no-cache', // Bypass cache
// cache: 'reload', // Force network
// cache: 'force-cache', // Use cache, even if stale
// Redirect
redirect: 'follow', // Follow redirects (default)
// redirect: 'error', // Error on redirect
// redirect: 'manual', // Handle redirects manually
// Referrer
referrer: 'about:client',
// Mode
mode: 'cors', // Cross-origin (default)
// mode: 'no-cors', // Limited response
// mode: 'same-origin', // Only same origin
// Integrity (Subresource Integrity)
integrity: 'sha256-...'
});
🛡️ Error Handling
// fetch() only rejects on network errors
fetch('/api/users')
.then(response => {
// HTTP errors (404, 500) don't throw!
console.log(response.ok); // false for 4xx, 5xx
})
.catch(error => {
// Only network errors reach here
console.error('Network error:', error);
});
// Check response.ok
async function fetchData() {
try {
let response = await fetch('/api/users');
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
let data = await response.json();
return data;
} catch (error) {
console.error('Error:', error.message);
throw error;
}
}
// Handle specific status codes
async function example() {
let response = await fetch('/api/users');
if (response.status === 404) {
console.log('Not found');
} else if (response.status === 401) {
console.log('Unauthorized');
redirectToLogin();
} else if (response.status === 500) {
console.log('Server error');
} else if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}
// Parse error response
async function handleError() {
try {
let response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: '' })
});
if (!response.ok) {
let error = await response.json();
throw new Error(error.message || 'Request failed');
}
return response.json();
} catch (error) {
console.error('Error:', error.message);
}
}
⏱️ Timeout and Abort
AbortController
// Abort fetch request
let controller = new AbortController();
fetch('/api/data', {
signal: controller.signal
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => {
if (error.name === 'AbortError') {
console.log('Fetch aborted');
} else {
console.error('Error:', error);
}
});
// Abort after some time
setTimeout(() => controller.abort(), 5000);
// Abort on user action
let abortBtn = document.querySelector('#abortBtn');
abortBtn.addEventListener('click', () => {
controller.abort();
});
Timeout Implementation
// Fetch with timeout
async function fetchWithTimeout(url, timeout = 5000) {
let controller = new AbortController();
let id = setTimeout(() => controller.abort(), timeout);
try {
let response = await fetch(url, {
signal: controller.signal
});
clearTimeout(id);
return response;
} catch (error) {
clearTimeout(id);
if (error.name === 'AbortError') {
throw new Error('Request timeout');
}
throw error;
}
}
// Usage
try {
let response = await fetchWithTimeout('/api/data', 3000);
let data = await response.json();
console.log(data);
} catch (error) {
console.error(error.message);
}
// Reusable utility
function withTimeout(promise, timeout) {
return Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), timeout)
)
]);
}
let response = await withTimeout(fetch('/api/data'), 5000);
💡 Practical Examples
API Client Class
class API {
constructor(baseURL, defaultHeaders = {}) {
this.baseURL = baseURL;
this.defaultHeaders = defaultHeaders;
}
async request(endpoint, options = {}) {
let url = `${this.baseURL}${endpoint}`;
let config = {
...options,
headers: {
...this.defaultHeaders,
...options.headers
}
};
let response = await fetch(url, config);
if (!response.ok) {
let error = await response.json().catch(() => ({}));
throw new Error(error.message || `HTTP ${response.status}`);
}
return response.json();
}
get(endpoint, options) {
return this.request(endpoint, { ...options, method: 'GET' });
}
post(endpoint, data, options) {
return this.request(endpoint, {
...options,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
}
put(endpoint, data, options) {
return this.request(endpoint, {
...options,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
}
delete(endpoint, options) {
return this.request(endpoint, { ...options, method: 'DELETE' });
}
}
// Usage
let api = new API('https://api.example.com', {
'Authorization': 'Bearer token123'
});
let users = await api.get('/users');
let newUser = await api.post('/users', { name: 'John' });
await api.delete('/users/1');
File Upload
async function uploadFile(file) {
let formData = new FormData();
formData.append('file', file);
formData.append('name', file.name);
try {
let response = await fetch('/api/upload', {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error('Upload failed');
}
let result = await response.json();
console.log('Uploaded:', result.url);
return result;
} catch (error) {
console.error('Upload error:', error);
throw error;
}
}
// With progress
async function uploadWithProgress(file, onProgress) {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', e => {
if (e.lengthComputable) {
let percent = (e.loaded / e.total) * 100;
onProgress(percent);
}
});
xhr.addEventListener('load', () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error(`HTTP ${xhr.status}`));
}
});
xhr.addEventListener('error', () => {
reject(new Error('Upload failed'));
});
let formData = new FormData();
formData.append('file', file);
xhr.open('POST', '/api/upload');
xhr.send(formData);
});
}
// Usage
let fileInput = document.querySelector('input[type="file"]');
fileInput.addEventListener('change', async () => {
let file = fileInput.files[0];
try {
await uploadWithProgress(file, percent => {
console.log(`Upload: ${percent.toFixed(0)}%`);
});
console.log('Complete!');
} catch (error) {
console.error('Failed:', error);
}
});
Pagination
async function* fetchAllPages(baseUrl) {
let page = 1;
let hasMore = true;
while (hasMore) {
let response = await fetch(`${baseUrl}?page=${page}`);
let data = await response.json();
yield data.items;
hasMore = data.hasMore;
page++;
}
}
// Usage
for await (let items of fetchAllPages('/api/users')) {
console.log('Page:', items.length);
processItems(items);
}
// Or load all at once
async function loadAll(baseUrl) {
let allItems = [];
for await (let items of fetchAllPages(baseUrl)) {
allItems.push(...items);
}
return allItems;
}
let users = await loadAll('/api/users');
Retry Logic
async function fetchWithRetry(url, options = {}, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
let response = await fetch(url, options);
if (response.ok || response.status === 404) {
return response;
}
if (i === retries - 1) {
throw new Error(`HTTP ${response.status}`);
}
} catch (error) {
if (i === retries - 1) {
throw error;
}
// Exponential backoff
let delay = Math.pow(2, i) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// Usage
try {
let response = await fetchWithRetry('/api/flaky-endpoint', {}, 3);
let data = await response.json();
console.log(data);
} catch (error) {
console.error('Failed after retries:', error);
}
Parallel Requests
// Load multiple resources
async function loadDashboard() {
try {
let [users, posts, stats] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/stats').then(r => r.json())
]);
return { users, posts, stats };
} catch (error) {
console.error('Dashboard load failed:', error);
throw error;
}
}
// With error handling for each
async function loadDashboardSafe() {
let results = await Promise.allSettled([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/stats').then(r => r.json())
]);
let [usersResult, postsResult, statsResult] = results;
return {
users: usersResult.status === 'fulfilled' ? usersResult.value : [],
posts: postsResult.status === 'fulfilled' ? postsResult.value : [],
stats: statsResult.status === 'fulfilled' ? statsResult.value : {}
};
}
Request Caching
class CachedFetch {
constructor(ttl = 60000) {
this.cache = new Map();
this.ttl = ttl;
}
async fetch(url, options = {}) {
let key = `${url}:${JSON.stringify(options)}`;
if (this.cache.has(key)) {
let { data, timestamp } = this.cache.get(key);
if (Date.now() - timestamp < this.ttl) {
return data;
}
}
let response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
let data = await response.json();
this.cache.set(key, {
data,
timestamp: Date.now()
});
return data;
}
clear(url) {
if (url) {
for (let key of this.cache.keys()) {
if (key.startsWith(url)) {
this.cache.delete(key);
}
}
} else {
this.cache.clear();
}
}
}
// Usage
let cached = new CachedFetch(30000); // 30 second TTL
let users = await cached.fetch('/api/users');
let usersAgain = await cached.fetch('/api/users'); // From cache
cached.clear('/api/users'); // Clear cache
🎯 Key Takeaways
- Promise-based: fetch() returns Promise, use .then() or await
- Check response.ok: HTTP errors don't throw, must check manually
- Read Body Once: response.json() can only be called once per response
- Network Errors: Only network failures reject the promise
- AbortController: Cancel requests with signal option
- Headers Object: Use Headers() for complex header manipulation
- Body Formats: JSON, FormData, URLSearchParams, Blob, text
- CORS: Cross-origin requests require server CORS configuration