Introduction to Fetch API
The Fetch API represents a modern approach to making network requests in web applications. It provides a more powerful and flexible feature set than older technologies like XMLHttpRequest, with a cleaner, promise-based interface that integrates perfectly with async/await.
As a crucial part of the web platform, Fetch enables seamless communication between client-side applications and servers, forming the backbone of most modern web applications.
Evolution of Browser Network Requests
To appreciate Fetch, it's helpful to understand the evolution of browser-based network requests:
- Early days: Page refreshes for every interaction with the server
- XMLHttpRequest (2005): First enabled AJAX, but with a complex, callback-based API
- jQuery $.ajax (2006): Simplified XHR but still used callbacks and was not native
- Fetch API (2015): Native, promise-based API with cleaner syntax and better features
Think of this evolution like transportation: full page refreshes were like taking a new bus for every stop, XMLHttpRequest was like an early car with a manual transmission and complex controls, jQuery was like an automatic car that's easier to drive but still requires external technology, and Fetch is like a modern electric vehicle - sleek, efficient, and built into the platform.
Basic Fetch Usage
At its core, the Fetch API centers around the global fetch() function, which initiates a request and returns a Promise that resolves to the Response object representing the result of the request.
Simple GET Request
The most basic usage is a simple GET request to retrieve data:
// Basic GET request
fetch('https://api.example.com/data')
.then(response => {
// Check if the response was successful
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json(); // Parse JSON response
})
.then(data => {
console.log('Data received:', data);
})
.catch(error => {
console.error('Fetch error:', error);
});
Using Fetch with Async/Await
Fetch shines even more when combined with async/await syntax:
// Fetch with async/await
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
console.log('Data received:', data);
return data;
} catch (error) {
console.error('Fetch error:', error);
throw error; // Re-throw for caller handling
}
}
Key Components of Fetch
Response Processing Methods
The Response object provides several methods to handle different types of data:
response.json()- Parse response as JSONresponse.text()- Get response as textresponse.blob()- Get response as a binary Blobresponse.formData()- Get response as FormDataresponse.arrayBuffer()- Get response as ArrayBuffer
Each of these methods returns a Promise that resolves to the processed data. Think of them as different translators that convert the raw response into various useful formats.
Real-World Example: Weather Dashboard
// Weather dashboard using fetch
async function getWeatherData(city) {
const apiKey = 'your_api_key';
const url = `https://api.weatherapi.com/v1/current.json?key=${apiKey}&q=${encodeURIComponent(city)}`;
try {
const response = await fetch(url);
if (!response.ok) {
if (response.status === 404) {
throw new Error(`City "${city}" not found`);
}
throw new Error(`Weather API error: ${response.status}`);
}
const weatherData = await response.json();
displayWeather(weatherData);
} catch (error) {
console.error('Weather fetch failed:', error);
displayError(error.message);
}
}
// Usage:
document.getElementById('weather-form').addEventListener('submit', function(event) {
event.preventDefault();
const city = document.getElementById('city-input').value;
getWeatherData(city);
});
Configuring Fetch Requests
The fetch() function accepts a second parameter, an options object, that allows you to customize the request.
Common Request Options
// Configuring a fetch request
fetch(url, {
method: 'POST', // HTTP method (GET, POST, PUT, DELETE, etc.)
headers: {
'Content-Type': 'application/json', // Request headers
'Authorization': 'Bearer token123'
},
body: JSON.stringify(data), // Request body (for POST, PUT, etc.)
mode: 'cors', // CORS mode (cors, no-cors, same-origin)
credentials: 'include', // Controls sending of cookies (omit, same-origin, include)
cache: 'no-cache', // Cache control (default, no-cache, reload, force-cache)
redirect: 'follow', // Redirect handling (follow, error, manual)
referrerPolicy: 'no-referrer', // Referrer policy
integrity: 'sha256-hash', // Subresource integrity check
keepalive: false, // Keep connection alive after page unload
signal: abortController.signal // For request cancellation
});
Making Different Types of Requests
POST Request Example
// POST request with JSON data
async function createUser(userData) {
try {
const response = await fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(userData)
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const newUser = await response.json();
return newUser;
} catch (error) {
console.error('Create user failed:', error);
throw error;
}
}
PUT Request Example
// PUT request to update a resource
async function updateUser(userId, updates) {
try {
const response = await fetch(`https://api.example.com/users/${userId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(updates)
});
if (!response.ok) {
throw new Error(`Update failed! Status: ${response.status}`);
}
const updatedUser = await response.json();
return updatedUser;
} catch (error) {
console.error('Update user failed:', error);
throw error;
}
}
DELETE Request Example
// DELETE request
async function deleteUser(userId) {
try {
const response = await fetch(`https://api.example.com/users/${userId}`, {
method: 'DELETE'
});
if (!response.ok) {
throw new Error(`Delete failed! Status: ${response.status}`);
}
// Some APIs return no content on DELETE
if (response.status === 204) {
return { success: true };
}
return await response.json();
} catch (error) {
console.error('Delete user failed:', error);
throw error;
}
}
Working with Headers
The Headers interface allows you to perform various operations on request and response headers.
// Working with Headers
const headers = new Headers();
// Adding headers
headers.append('Content-Type', 'application/json');
headers.append('Authorization', 'Bearer token123');
// Alternative syntax
const headers2 = new Headers({
'Content-Type': 'application/json',
'Authorization': 'Bearer token123'
});
// Checking, getting, and setting headers
if (headers.has('Authorization')) {
console.log(headers.get('Authorization')); // "Bearer token123"
}
headers.set('Accept', 'application/json'); // Set or replace a header
// Using headers in fetch
fetch(url, { headers: headers })
Real-World Example: E-commerce API
// E-commerce API client
class EcommerceAPI {
constructor(baseUrl, apiKey) {
this.baseUrl = baseUrl;
this.apiKey = apiKey;
}
async getHeaders() {
// Real-world apps might fetch tokens from storage or auth services
return {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`,
'X-Requested-With': 'XMLHttpRequest'
};
}
async getProducts(category, limit = 10) {
const url = new URL(`${this.baseUrl}/products`);
url.searchParams.append('category', category);
url.searchParams.append('limit', limit.toString());
const response = await fetch(url, {
headers: await this.getHeaders()
});
if (!response.ok) {
throw new Error(`Failed to fetch products: ${response.status}`);
}
return response.json();
}
async addToCart(productId, quantity) {
const response = await fetch(`${this.baseUrl}/cart/items`, {
method: 'POST',
headers: await this.getHeaders(),
body: JSON.stringify({
product_id: productId,
quantity: quantity
})
});
if (!response.ok) {
throw new Error(`Failed to add item to cart: ${response.status}`);
}
return response.json();
}
async checkout(paymentDetails) {
const response = await fetch(`${this.baseUrl}/orders`, {
method: 'POST',
headers: await this.getHeaders(),
body: JSON.stringify({
payment: paymentDetails,
shipping_address: paymentDetails.address
})
});
if (!response.ok) {
throw new Error(`Checkout failed: ${response.status}`);
}
return response.json();
}
}
Understanding the Response Object
The Response object provides essential information about the response received from the server. Understanding its properties and methods is crucial for effective fetch usage.
Key Response Properties
response.ok- Boolean indicating if status is in the successful range (200-299)response.status- HTTP status code (200, 404, 500, etc.)response.statusText- HTTP status messageresponse.headers- Headers object containing response headersresponse.url- URL of the response (may differ from requested URL due to redirects)response.type- Type of response (basic, cors, error, etc.)response.redirected- Whether the response is the result of a redirect
// Working with response properties
async function fetchWithDetailedResponse(url) {
try {
const response = await fetch(url);
console.log('Response details:');
console.log(`Status: ${response.status} ${response.statusText}`);
console.log(`Success: ${response.ok}`);
console.log(`URL: ${response.url}`);
console.log(`Type: ${response.type}`);
console.log(`Redirected: ${response.redirected}`);
// Log response headers
console.log('Response headers:');
for (const [key, value] of response.headers) {
console.log(`${key}: ${value}`);
}
// Only parse JSON if response was successful
if (response.ok) {
const data = await response.json();
return data;
} else {
throw new Error(`Request failed with status: ${response.status}`);
}
} catch (error) {
console.error('Fetch failed:', error);
throw error;
}
}
Error Handling Best Practices
One common gotcha with Fetch is that it only rejects the promise on network errors. HTTP error statuses like 404 or 500 do not cause promise rejection by themselves.
// Comprehensive error handling
async function fetchWithErrorHandling(url) {
try {
const response = await fetch(url);
// Check for HTTP errors
if (!response.ok) {
let errorMessage = `HTTP error! Status: ${response.status}`;
// Try to extract more error details if available
try {
const errorData = await response.json();
if (errorData.message) {
errorMessage = errorData.message;
} else if (errorData.error) {
errorMessage = errorData.error;
}
} catch {
// If parsing error JSON fails, use the default error message
}
// Create an error with additional information
const error = new Error(errorMessage);
error.status = response.status;
error.statusText = response.statusText;
error.response = response;
throw error;
}
return await response.json();
} catch (error) {
// Handle network errors
if (!error.status) {
console.error('Network error or CORS issue:', error);
} else {
console.error(`HTTP error ${error.status}: ${error.message}`);
}
throw error;
}
}
Response Status Codes Guide
Understanding common HTTP status codes is essential for proper error handling:
Real-World Response Handling
// Advanced API response handler
async function apiRequest(url, options = {}) {
// Set default headers
options.headers = {
'Content-Type': 'application/json',
'Accept': 'application/json',
...options.headers
};
try {
const response = await fetch(url, options);
// Handle different status codes appropriately
switch (response.status) {
case 200:
case 201:
return await response.json();
case 204: // No content
return { success: true };
case 401:
// Unauthorized - maybe token expired
logoutUser();
throw new Error('Your session has expired. Please login again.');
case 403:
throw new Error('You do not have permission to access this resource.');
case 404:
throw new Error('The requested resource was not found.');
case 422: // Validation errors
const validationErrors = await response.json();
throw {
message: 'Validation failed',
errors: validationErrors.errors,
status: 422
};
case 429:
throw new Error('Rate limit exceeded. Please try again later.');
case 500:
case 502:
case 503:
case 504:
throw new Error('Server error. Please try again later.');
default:
throw new Error(`Request failed with status: ${response.status}`);
}
} catch (error) {
// Log error for monitoring
reportErrorToMonitoring(error, { url, ...options });
// Re-throw for caller
throw error;
}
}
Working with Different Data Formats
JSON: The Most Common Format
JSON (JavaScript Object Notation) is the most widely used data format in web APIs due to its simplicity and JavaScript compatibility.
// Working with JSON data
async function fetchUserProfile(userId) {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`Failed to fetch user: ${response.status}`);
}
// Parse JSON response
const user = await response.json();
return user;
}
// Sending JSON data
async function updateProfile(userId, profileData) {
const response = await fetch(`/api/users/${userId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(profileData) // Convert JS object to JSON string
});
if (!response.ok) {
throw new Error(`Failed to update profile: ${response.status}`);
}
return response.json();
}
Working with Form Data
The FormData API provides a way to easily construct key/value pairs representing form fields and values, which can be sent using fetch.
// Submitting a form with FormData
async function submitForm(formElement) {
// Create FormData from existing form element
const formData = new FormData(formElement);
// Optionally add additional fields
formData.append('timestamp', Date.now());
// Log form data (for debugging)
for (const [key, value] of formData.entries()) {
console.log(`${key}: ${value}`);
}
const response = await fetch('/api/submit-form', {
method: 'POST',
body: formData
// Note: Don't set Content-Type header, browser sets it with boundary
});
if (!response.ok) {
throw new Error(`Form submission failed: ${response.status}`);
}
return response.json();
}
// Manual FormData creation
async function uploadUserFiles(userId, fileList) {
const formData = new FormData();
// Add user ID as a field
formData.append('userId', userId);
// Add each selected file
for (const file of fileList) {
formData.append('files', file);
}
// Add metadata as JSON
const metadata = { type: 'user_upload', count: fileList.length };
formData.append('metadata', JSON.stringify(metadata));
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error(`Upload failed: ${response.status}`);
}
return response.json();
}
Working with Text and Binary Data
While JSON is common, sometimes you need to work with plain text or binary data.
// Fetching text data (like HTML, CSS, or plain text)
async function fetchTemplate(templateName) {
const response = await fetch(`/templates/${templateName}.html`);
if (!response.ok) {
throw new Error(`Template not found: ${response.status}`);
}
// Get response as text
return response.text();
}
// Working with binary data (like images)
async function fetchAndDisplayImage(imageUrl) {
const response = await fetch(imageUrl);
if (!response.ok) {
throw new Error(`Failed to fetch image: ${response.status}`);
}
// Get the image as a blob
const imageBlob = await response.blob();
// Create a URL for the blob
const imageObjectURL = URL.createObjectURL(imageBlob);
// Display the image
const imageElement = document.getElementById('myImage');
imageElement.src = imageObjectURL;
// Clean up the URL when done
imageElement.onload = () => {
URL.revokeObjectURL(imageObjectURL);
};
}
Real-World Example: File Upload with Progress
Combining Fetch API with other browser APIs can enable advanced functionality like upload progress tracking.
// File upload with progress tracking
async function uploadFileWithProgress(file, onProgress) {
// Create a FormData instance
const formData = new FormData();
formData.append('file', file);
// Create an XMLHttpRequest (needed for progress tracking)
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
// Setup progress event
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
const percentComplete = (event.loaded / event.total) * 100;
onProgress(percentComplete);
}
});
// Setup completion handler
xhr.addEventListener('load', () => {
if (xhr.status >= 200 && xhr.status < 300) {
try {
const response = JSON.parse(xhr.responseText);
resolve(response);
} catch (e) {
resolve({ success: true });
}
} else {
reject(new Error(`Upload failed with status: ${xhr.status}`));
}
});
// Setup error handler
xhr.addEventListener('error', () => {
reject(new Error('Network error during upload'));
});
// Open and send the request
xhr.open('POST', '/api/upload');
xhr.send(formData);
});
}
// Usage:
const fileInput = document.getElementById('fileInput');
const progressBar = document.getElementById('progressBar');
const uploadButton = document.getElementById('uploadButton');
uploadButton.addEventListener('click', async () => {
if (fileInput.files.length === 0) {
alert('Please select a file');
return;
}
try {
const file = fileInput.files[0];
// Update progress while uploading
await uploadFileWithProgress(file, (percent) => {
progressBar.style.width = `${percent}%`;
progressBar.textContent = `${Math.round(percent)}%`;
});
alert('Upload completed successfully!');
} catch (error) {
console.error('Upload error:', error);
alert(`Upload failed: ${error.message}`);
}
});
Advanced Fetch Features
Request Cancellation
The AbortController API allows you to cancel in-flight fetch requests, which is useful for implementing timeouts, canceling requests when a user navigates away, or replacing pending requests with newer ones.
// Cancellable fetch request
async function fetchWithTimeout(url, options = {}, timeoutMs = 5000) {
// Create abort controller
const controller = new AbortController();
const { signal } = controller;
// Set up timeout
const timeoutId = setTimeout(() => {
controller.abort();
}, timeoutMs);
try {
// Add signal to fetch options
const response = await fetch(url, { ...options, signal });
// Clear timeout since request completed
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return await response.json();
} catch (error) {
// Clear timeout in case of error
clearTimeout(timeoutId);
// Check if it's an abort error
if (error.name === 'AbortError') {
throw new Error(`Request timed out after ${timeoutMs}ms`);
}
throw error;
}
}
// Usage example: Search with cancellation
let currentSearchController = null;
async function searchProducts(query) {
// Cancel any ongoing search request
if (currentSearchController) {
currentSearchController.abort();
}
// Create a new controller for this request
currentSearchController = new AbortController();
try {
const results = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
signal: currentSearchController.signal
}).then(res => res.json());
displayResults(results);
} catch (error) {
if (error.name === 'AbortError') {
console.log('Search cancelled in favor of new search');
} else {
console.error('Search error:', error);
}
}
}
Handling CORS Issues
Cross-Origin Resource Sharing (CORS) is a security feature implemented by browsers that restricts web pages from making requests to a different domain than the one that served the page.
// CORS configuration
fetch('https://api.example.com/data', {
method: 'GET',
mode: 'cors', // 'cors', 'no-cors', 'same-origin'
credentials: 'include', // 'omit', 'same-origin', 'include'
headers: {
'Content-Type': 'application/json'
}
});
Common CORS issues and solutions:
- Server needs CORS headers: The server must include appropriate Access-Control-* headers
- Preflight requests: Complex requests trigger an OPTIONS request that must be properly handled
- Credentials handling: Sending cookies cross-origin requires special configuration
- Proxying requests: In development, proxy servers can bypass CORS restrictions
Cache Control
The cache option in fetch allows control over how the request interacts with the browser's HTTP cache.
// Cache control options
fetch('/api/data', {
cache: 'default', // Normal browser caching
cache: 'no-cache', // Validate cache with server before using
cache: 'reload', // Bypass cache completely
cache: 'force-cache', // Use cache even if expired
cache: 'only-if-cached' // Use cache only (offline mode)
});
Real-World Example: Implementing an API Client
// Advanced API client with fetch
class APIClient {
constructor(baseUrl, options = {}) {
this.baseUrl = baseUrl;
this.defaultOptions = {
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
timeout: 30000,
retries: 3,
...options
};
}
// Helper for making requests with retries and timeouts
async request(endpoint, options = {}) {
const url = `${this.baseUrl}${endpoint}`;
const requestOptions = {
...this.defaultOptions,
...options,
headers: {
...this.defaultOptions.headers,
...options.headers
}
};
// Extract non-fetch options
const { timeout, retries, ...fetchOptions } = requestOptions;
// Initialize counters
let attempt = 0;
let lastError = null;
while (attempt < retries) {
attempt++;
const controller = new AbortController();
// Set timeout if specified
const timeoutId = timeout
? setTimeout(() => controller.abort(), timeout)
: null;
try {
const response = await fetch(url, {
...fetchOptions,
signal: controller.signal
});
// Clear timeout if it was set
if (timeoutId) clearTimeout(timeoutId);
// Handle successful response
if (response.ok) {
// Check if response has content
const contentType = response.headers.get('Content-Type');
if (contentType && contentType.includes('application/json')) {
return await response.json();
} else if (response.status === 204) {
return { success: true };
} else {
return await response.text();
}
}
// Handle authentication issues
if (response.status === 401) {
// Possibly refresh tokens or logout
this.handleAuthError();
throw new Error('Authentication failed');
}
// For other error statuses, try to get error details
let errorData;
try {
errorData = await response.json();
} catch {
errorData = { message: response.statusText };
}
// Determine if we should retry based on status
if (response.status >= 500 && attempt < retries) {
// Server error, retry after delay
lastError = new Error(errorData.message || `Server error: ${response.status}`);
lastError.status = response.status;
await this.delay(this.getBackoffTime(attempt));
continue;
}
// Other errors we don't retry
const error = new Error(errorData.message || `HTTP error: ${response.status}`);
error.status = response.status;
error.data = errorData;
throw error;
} catch (error) {
// Clear timeout if set
if (timeoutId) clearTimeout(timeoutId);
// Determine if we should retry
if (error.name === 'AbortError') {
if (attempt < retries) {
lastError = new Error('Request timeout');
await this.delay(this.getBackoffTime(attempt));
continue;
}
throw new Error(`Request timed out after ${timeout}ms`);
}
// Network errors can be retried
if (error.message.includes('NetworkError') && attempt < retries) {
lastError = error;
await this.delay(this.getBackoffTime(attempt));
continue;
}
// Rethrow other errors
throw error;
}
}
// If we got here, we failed after all retries
throw lastError || new Error('Request failed after multiple attempts');
}
// Helper for exponential backoff
getBackoffTime(attempt) {
return Math.min(1000 * Math.pow(2, attempt - 1), 10000);
}
// Helper to create a delay
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Auth error handler
handleAuthError() {
// Implement token refresh or logout logic
console.log('Auth error handled');
}
// Convenience methods for different HTTP methods
async get(endpoint, options = {}) {
return this.request(endpoint, { ...options, method: 'GET' });
}
async post(endpoint, data, options = {}) {
return this.request(endpoint, {
...options,
method: 'POST',
body: JSON.stringify(data)
});
}
async put(endpoint, data, options = {}) {
return this.request(endpoint, {
...options,
method: 'PUT',
body: JSON.stringify(data)
});
}
async delete(endpoint, options = {}) {
return this.request(endpoint, { ...options, method: 'DELETE' });
}
}
Practical Patterns and Best Practices
Error Handling Patterns
Consistent error handling is crucial for a good user experience.
// Error handling pattern
async function fetchWithErrorHandling(url, options = {}) {
const { errorHandler, ...fetchOptions } = options;
try {
const response = await fetch(url, fetchOptions);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
const error = new Error(errorData.message || `HTTP error ${response.status}`);
error.status = response.status;
error.data = errorData;
// Call custom error handler if provided
if (errorHandler) {
errorHandler(error);
}
throw error;
}
return response;
} catch (error) {
// Handle network errors
if (!error.status) {
const networkError = new Error('Network error, please check your connection');
networkError.originalError = error;
if (errorHandler) {
errorHandler(networkError);
}
throw networkError;
}
// Re-throw HTTP errors (already handled above)
throw error;
}
}
Authentication Patterns
Most applications need to handle authentication in their API requests.
// Authentication pattern
class AuthenticatedAPI {
constructor(baseUrl) {
this.baseUrl = baseUrl;
}
// Get auth token (could retrieve from localStorage, context, etc.)
getAuthToken() {
return localStorage.getItem('authToken');
}
// Refresh token if needed
async refreshTokenIfNeeded() {
const tokenData = JSON.parse(localStorage.getItem('tokenData') || '{}');
// Check if token is expired or close to expiring
if (tokenData.expiresAt && new Date(tokenData.expiresAt) < new Date(Date.now() + 5 * 60 * 1000)) {
// Token is expired or expires in less than 5 minutes, refresh it
try {
const response = await fetch(`${this.baseUrl}/auth/refresh`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ refreshToken: tokenData.refreshToken })
});
if (!response.ok) {
// If refresh fails, redirect to login
this.redirectToLogin();
throw new Error('Token refresh failed');
}
const newTokenData = await response.json();
// Save new token data
localStorage.setItem('authToken', newTokenData.accessToken);
localStorage.setItem('tokenData', JSON.stringify({
refreshToken: newTokenData.refreshToken,
expiresAt: newTokenData.expiresAt
}));
} catch (error) {
console.error('Token refresh error:', error);
this.redirectToLogin();
throw error;
}
}
}
// Redirect to login page
redirectToLogin() {
localStorage.removeItem('authToken');
localStorage.removeItem('tokenData');
window.location.href = '/login';
}
// Make authenticated request
async request(endpoint, options = {}) {
// First check if token needs refreshing
await this.refreshTokenIfNeeded();
// Get the current token
const token = this.getAuthToken();
if (!token) {
this.redirectToLogin();
throw new Error('No authentication token available');
}
// Add auth header to request
const authOptions = {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${token}`
}
};
try {
const response = await fetch(`${this.baseUrl}${endpoint}`, authOptions);
// Handle authentication errors
if (response.status === 401) {
this.redirectToLogin();
throw new Error('Authentication failed');
}
if (!response.ok) {
throw new Error(`Request failed with status: ${response.status}`);
}
return response.json();
} catch (error) {
console.error('API request failed:', error);
throw error;
}
}
// Convenience methods
async get(endpoint, options = {}) {
return this.request(endpoint, { ...options, method: 'GET' });
}
async post(endpoint, data, options = {}) {
return this.request(endpoint, {
...options,
method: 'POST',
headers: {
'Content-Type': 'application/json',
...options.headers
},
body: JSON.stringify(data)
});
}
}
Rate Limiting and Throttling
To prevent overwhelming APIs and avoid hitting rate limits, implement throttling for your requests.
// Throttled fetch with queue
class ThrottledFetcher {
constructor(requestsPerSecond = 5) {
this.queue = [];
this.processing = false;
this.requestsPerSecond = requestsPerSecond;
this.minTimeBetweenRequests = 1000 / requestsPerSecond;
this.lastRequestTime = 0;
}
async fetch(url, options = {}) {
// Create a promise that will be resolved when the request completes
return new Promise((resolve, reject) => {
// Add to queue
this.queue.push({
url,
options,
resolve,
reject
});
// Start processing if not already
if (!this.processing) {
this.processQueue();
}
});
}
async processQueue() {
this.processing = true;
while (this.queue.length > 0) {
const { url, options, resolve, reject } = this.queue.shift();
try {
// Calculate time to wait
const now = Date.now();
const timeToWait = Math.max(0, this.lastRequestTime + this.minTimeBetweenRequests - now);
// Wait if needed
if (timeToWait > 0) {
await new Promise(r => setTimeout(r, timeToWait));
}
// Make the request
this.lastRequestTime = Date.now();
const response = await fetch(url, options);
resolve(response);
} catch (error) {
reject(error);
}
}
this.processing = false;
}
}
Batching Requests
Sometimes it's more efficient to batch multiple requests into a single HTTP request.
// Request batching
class BatchingAPI {
constructor(batchEndpoint, maxBatchSize = 10, batchInterval = 100) {
this.batchEndpoint = batchEndpoint;
this.maxBatchSize = maxBatchSize;
this.batchInterval = batchInterval;
this.batch = [];
this.batchPromises = [];
this.batchTimeout = null;
}
request(path, method = 'GET', body = null) {
return new Promise((resolve, reject) => {
// Add to current batch
this.batch.push({
path,
method,
body,
resolve,
reject
});
this.batchPromises.push({ resolve, reject });
// If we've reached max batch size, send immediately
if (this.batch.length >= this.maxBatchSize) {
this.sendBatch();
} else if (!this.batchTimeout) {
// Otherwise schedule a batch send
this.batchTimeout = setTimeout(() => this.sendBatch(), this.batchInterval);
}
});
}
async sendBatch() {
// Clear timeout
if (this.batchTimeout) {
clearTimeout(this.batchTimeout);
this.batchTimeout = null;
}
// If nothing to send, return
if (this.batch.length === 0) return;
// Take the current batch and reset
const currentBatch = [...this.batch];
const currentPromises = [...this.batchPromises];
this.batch = [];
this.batchPromises = [];
try {
// Prepare batch payload
const batchPayload = currentBatch.map((request, index) => ({
id: index.toString(),
path: request.path,
method: request.method,
body: request.body
}));
// Send batch request
const response = await fetch(this.batchEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(batchPayload)
});
if (!response.ok) {
throw new Error(`Batch request failed with status: ${response.status}`);
}
// Process batch response
const batchResults = await response.json();
// Resolve/reject individual promises
batchResults.forEach((result, index) => {
if (result.error) {
currentPromises[index].reject(new Error(result.error));
} else {
currentPromises[index].resolve(result.data);
}
});
} catch (error) {
// If entire batch fails, reject all promises
currentPromises.forEach(({ reject }) => {
reject(error);
});
}
}
}
Browser Support and Polyfills
The Fetch API is widely supported in modern browsers, but older browsers (particularly Internet Explorer) do not support it natively.
Browser Compatibility
- Chrome: 42+ (April 2015)
- Firefox: 39+ (July 2015)
- Safari: 10.1+ (March 2017)
- Edge: 14+ (August 2016)
- Internet Explorer: No native support
Using Polyfills
For older browsers, you can use a polyfill like github/fetch or whatwg-fetch.
// Using a fetch polyfill
// First, import the polyfill
import 'whatwg-fetch';
// Then use fetch normally
fetch('/api/data')
.then(response => response.json())
.then(data => console.log(data));
Modern Alternatives
For complex API requirements, consider utility libraries like axios or ky that build on top of Fetch and provide additional features.
// Using axios (compared to Fetch)
// Axios automatically transforms JSON and has simpler error handling
axios.get('/api/data')
.then(response => {
// response.data is already parsed JSON
console.log(response.data);
})
.catch(error => {
// HTTP errors are caught here automatically
console.error('Error:', error);
});
Practice Exercises
Exercise 1: Basic Data Fetching
Create a function that fetches data from the provided URL, properly handles errors, and returns the parsed data.
Exercise 2: Building an API Client
Create a reusable API client class that abstracts common operations like authentication, error handling, and request configuration.
Exercise 3: File Upload with Progress
Implement a file upload feature that shows upload progress and handles errors gracefully.
Exercise 4: Cancellable Search
Build a search function that cancels previous requests when a new search is initiated to avoid race conditions.
Exercise 5: Pagination with Infinite Scroll
Implement an infinite scroll feature that loads additional data when the user scrolls near the bottom of the page.
Summary and Key Takeaways
The Fetch API provides a powerful and flexible way to make network requests in modern web applications. Key points to remember:
- Promise-based API: Fetch returns a Promise, making it work seamlessly with async/await.
- Two-stage processing: First get the Response object, then process the body with methods like json(), text(), etc.
- Manual error handling: Fetch only rejects on network errors, so you must check response.ok for HTTP error status codes.
- Configurability: The options object provides extensive control over headers, method, body, mode, credentials, etc.
- Modern features: AbortController for cancellation, FormData for form submissions, and comprehensive header support.
- Excellent ecosystem: Easy to create utility functions, wrappers, and integration with other browser APIs.
The Fetch API's combination of simplicity and power makes it the go-to choice for HTTP requests in modern web development, especially when used with async/await for clean, readable code.