How to fetch data from an API with fetch()

How to Fetch Data from an API with fetch() Table of Contents 1. [Introduction](#introduction) 2. [Prerequisites](#prerequisites) 3. [Understanding the fetch() API](#understanding-the-fetch-api) 4. [Basic Syntax and Structure](#basic-syntax-and-structure) 5. [Making Your First API Request](#making-your-first-api-request) 6. [Handling Different HTTP Methods](#handling-different-http-methods) 7. [Working with Request Headers](#working-with-request-headers) 8. [Error Handling and Response Management](#error-handling-and-response-management) 9. [Advanced fetch() Techniques](#advanced-fetch-techniques) 10. [Real-World Examples](#real-world-examples) 11. [Common Issues and Troubleshooting](#common-issues-and-troubleshooting) 12. [Best Practices and Professional Tips](#best-practices-and-professional-tips) 13. [Performance Optimization](#performance-optimization) 14. [Conclusion](#conclusion) Introduction The `fetch()` API is a modern, powerful, and flexible method for making HTTP requests in JavaScript. It provides a cleaner, more intuitive alternative to the older `XMLHttpRequest` object and has become the standard way to interact with APIs in web development. This comprehensive guide will teach you everything you need to know about using `fetch()` to retrieve data from APIs, handle responses, manage errors, and implement best practices for robust web applications. Whether you're building a simple weather app, integrating with social media APIs, or developing complex enterprise applications, mastering the `fetch()` API is essential for modern JavaScript development. By the end of this article, you'll have a thorough understanding of how to effectively use `fetch()` in various scenarios and handle common challenges that arise when working with external APIs. Prerequisites Before diving into the `fetch()` API, ensure you have: - Basic JavaScript Knowledge: Understanding of variables, functions, objects, and arrays - Promise Fundamentals: Familiarity with JavaScript Promises and asynchronous programming - ES6+ Syntax: Knowledge of arrow functions, destructuring, and template literals - HTTP Basics: Understanding of HTTP methods (GET, POST, PUT, DELETE) and status codes - JSON Format: Ability to work with JSON data structures - Development Environment: A code editor and browser with developer tools - Basic HTML/CSS: For creating simple test pages to demonstrate concepts Understanding the fetch() API The `fetch()` API is a Web API that provides a JavaScript interface for accessing and manipulating parts of the HTTP pipeline, such as requests and responses. It's built on top of Promises, making it naturally asynchronous and perfect for modern JavaScript development patterns. Key Features of fetch() - Promise-based: Returns Promises for cleaner asynchronous code - Flexible: Supports various request methods and configurations - Streaming: Can handle response bodies as streams - CORS-aware: Built-in support for Cross-Origin Resource Sharing - Modern: Designed for contemporary web development practices Browser Support The `fetch()` API is supported in all modern browsers: - Chrome 42+ - Firefox 39+ - Safari 10.1+ - Edge 14+ For older browsers, you can use polyfills like `whatwg-fetch`. Basic Syntax and Structure The basic syntax of `fetch()` is straightforward: ```javascript fetch(url, options) .then(response => response.json()) .then(data => console.log(data)) .catch(error => console.error('Error:', error)); ``` Parameters 1. url (required): The URL of the resource you want to fetch 2. options (optional): An object containing request configuration Return Value `fetch()` returns a Promise that resolves to a Response object representing the response to the request. Making Your First API Request Let's start with a simple GET request to fetch data from a public API: ```javascript // Basic GET request fetch('https://jsonplaceholder.typicode.com/posts/1') .then(response => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); }) .then(data => { console.log('Success:', data); // Handle the data here document.getElementById('title').textContent = data.title; document.getElementById('body').textContent = data.body; }) .catch(error => { console.error('Error:', error); }); ``` Using async/await Syntax For cleaner, more readable code, you can use async/await: ```javascript async function fetchPost() { try { const response = await fetch('https://jsonplaceholder.typicode.com/posts/1'); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); console.log('Success:', data); return data; } catch (error) { console.error('Error:', error); throw error; } } // Usage fetchPost().then(data => { // Handle successful response }).catch(error => { // Handle error }); ``` Handling Different HTTP Methods GET Requests GET requests are the default method for `fetch()`: ```javascript // Explicit GET request const getData = async () => { try { const response = await fetch('https://api.example.com/data', { method: 'GET', headers: { 'Content-Type': 'application/json', } }); return await response.json(); } catch (error) { console.error('GET request failed:', error); } }; ``` POST Requests POST requests are used to send data to the server: ```javascript const postData = async (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}`); } return await response.json(); } catch (error) { console.error('POST request failed:', error); throw error; } }; // Usage const newUser = { name: 'John Doe', email: 'john@example.com', age: 30 }; postData(newUser) .then(result => console.log('User created:', result)) .catch(error => console.error('Failed to create user:', error)); ``` PUT Requests PUT requests are used to update existing resources: ```javascript const updateData = async (id, updatedData) => { try { const response = await fetch(`https://api.example.com/users/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(updatedData) }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return await response.json(); } catch (error) { console.error('PUT request failed:', error); throw error; } }; ``` DELETE Requests DELETE requests are used to remove resources: ```javascript const deleteData = async (id) => { try { const response = await fetch(`https://api.example.com/users/${id}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json', } }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.status === 204 ? null : await response.json(); } catch (error) { console.error('DELETE request failed:', error); throw error; } }; ``` Working with Request Headers Headers provide additional information about the request or response. Here's how to work with them effectively: Setting Request Headers ```javascript const fetchWithHeaders = async () => { try { const response = await fetch('https://api.example.com/protected-data', { method: 'GET', headers: { 'Authorization': 'Bearer your-token-here', 'Content-Type': 'application/json', 'Accept': 'application/json', 'User-Agent': 'MyApp/1.0', 'X-Custom-Header': 'custom-value' } }); return await response.json(); } catch (error) { console.error('Request with headers failed:', error); } }; ``` Dynamic Headers ```javascript const createAuthenticatedRequest = (token) => { const headers = new Headers(); headers.append('Authorization', `Bearer ${token}`); headers.append('Content-Type', 'application/json'); return { method: 'GET', headers: headers }; }; // Usage const token = 'your-jwt-token'; const options = createAuthenticatedRequest(token); fetch('https://api.example.com/user-profile', options) .then(response => response.json()) .then(data => console.log(data)); ``` Reading Response Headers ```javascript const checkResponseHeaders = async () => { try { const response = await fetch('https://api.example.com/data'); // Check specific headers console.log('Content-Type:', response.headers.get('Content-Type')); console.log('Cache-Control:', response.headers.get('Cache-Control')); // Iterate through all headers for (let [key, value] of response.headers) { console.log(`${key}: ${value}`); } return await response.json(); } catch (error) { console.error('Error reading headers:', error); } }; ``` Error Handling and Response Management Proper error handling is crucial for robust applications. The `fetch()` API has some unique characteristics when it comes to error handling. Understanding fetch() Error Behavior Important: `fetch()` only rejects the Promise for network errors, not HTTP error status codes (like 404 or 500). ```javascript const robustFetch = async (url, options = {}) => { try { const response = await fetch(url, options); // Check if the response is ok (status in the range 200-299) if (!response.ok) { // Handle different HTTP status codes switch (response.status) { case 400: throw new Error('Bad Request - Check your request parameters'); case 401: throw new Error('Unauthorized - Check your authentication'); case 403: throw new Error('Forbidden - You do not have permission'); case 404: throw new Error('Not Found - The requested resource does not exist'); case 500: throw new Error('Internal Server Error - Please try again later'); default: throw new Error(`HTTP Error: ${response.status} ${response.statusText}`); } } return response; } catch (error) { // Handle network errors and our custom errors if (error instanceof TypeError) { throw new Error('Network error - Check your internet connection'); } throw error; } }; ``` Comprehensive Error Handling Example ```javascript const fetchDataWithErrorHandling = async (url) => { try { const response = await robustFetch(url); // Check content type before parsing const contentType = response.headers.get('content-type'); if (!contentType || !contentType.includes('application/json')) { throw new Error('Response is not JSON'); } const data = await response.json(); return data; } catch (error) { console.error('Fetch operation failed:', error.message); // You might want to show user-friendly messages const userMessage = getUserFriendlyMessage(error.message); showErrorToUser(userMessage); throw error; // Re-throw if you want calling code to handle it } }; const getUserFriendlyMessage = (errorMessage) => { if (errorMessage.includes('Network error')) { return 'Please check your internet connection and try again.'; } else if (errorMessage.includes('Unauthorized')) { return 'Please log in to access this content.'; } else if (errorMessage.includes('Not Found')) { return 'The requested information could not be found.'; } return 'Something went wrong. Please try again later.'; }; const showErrorToUser = (message) => { // Implementation depends on your UI framework console.error('User Error:', message); // Example: document.getElementById('error-message').textContent = message; }; ``` Advanced fetch() Techniques Request Timeout `fetch()` doesn't have a built-in timeout option, but you can implement one using `AbortController`: ```javascript const fetchWithTimeout = async (url, options = {}, timeout = 5000) => { const controller = new AbortController(); const id = setTimeout(() => controller.abort(), timeout); try { const response = await fetch(url, { ...options, signal: controller.signal }); clearTimeout(id); return response; } catch (error) { clearTimeout(id); if (error.name === 'AbortError') { throw new Error(`Request timeout after ${timeout}ms`); } throw error; } }; // Usage fetchWithTimeout('https://api.example.com/slow-endpoint', {}, 3000) .then(response => response.json()) .then(data => console.log(data)) .catch(error => console.error('Request failed:', error.message)); ``` Request Cancellation ```javascript class APIClient { constructor() { this.activeRequests = new Map(); } async fetch(key, url, options = {}) { // Cancel previous request with the same key if (this.activeRequests.has(key)) { this.activeRequests.get(key).abort(); } const controller = new AbortController(); this.activeRequests.set(key, controller); try { const response = await fetch(url, { ...options, signal: controller.signal }); this.activeRequests.delete(key); return response; } catch (error) { this.activeRequests.delete(key); throw error; } } cancelRequest(key) { if (this.activeRequests.has(key)) { this.activeRequests.get(key).abort(); this.activeRequests.delete(key); } } cancelAllRequests() { for (let [key, controller] of this.activeRequests) { controller.abort(); } this.activeRequests.clear(); } } // Usage const apiClient = new APIClient(); // Start a request apiClient.fetch('user-data', 'https://api.example.com/user') .then(response => response.json()) .then(data => console.log(data)) .catch(error => { if (error.name !== 'AbortError') { console.error('Request failed:', error); } }); // Cancel the request if needed // apiClient.cancelRequest('user-data'); ``` Retry Logic ```javascript const fetchWithRetry = async (url, options = {}, maxRetries = 3, delay = 1000) => { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { const response = await fetch(url, options); if (response.ok) { return response; } // Don't retry for client errors (4xx) if (response.status >= 400 && response.status < 500) { throw new Error(`Client error: ${response.status}`); } // Retry for server errors (5xx) and other issues if (attempt === maxRetries) { throw new Error(`Server error after ${maxRetries} attempts: ${response.status}`); } console.log(`Attempt ${attempt} failed, retrying in ${delay}ms...`); await new Promise(resolve => setTimeout(resolve, delay)); delay *= 2; // Exponential backoff } catch (error) { if (attempt === maxRetries) { throw error; } console.log(`Attempt ${attempt} failed, retrying in ${delay}ms...`); await new Promise(resolve => setTimeout(resolve, delay)); delay *= 2; } } }; ``` Real-World Examples Weather App Integration ```javascript class WeatherService { constructor(apiKey) { this.apiKey = apiKey; this.baseUrl = 'https://api.openweathermap.org/data/2.5'; } async getCurrentWeather(city) { const url = `${this.baseUrl}/weather?q=${encodeURIComponent(city)}&appid=${this.apiKey}&units=metric`; try { const response = await fetch(url); if (!response.ok) { if (response.status === 404) { throw new Error('City not found'); } throw new Error(`Weather API error: ${response.status}`); } const data = await response.json(); return { city: data.name, country: data.sys.country, temperature: data.main.temp, description: data.weather[0].description, humidity: data.main.humidity, windSpeed: data.wind.speed }; } catch (error) { console.error('Failed to fetch weather data:', error); throw error; } } async getForecast(city, days = 5) { const url = `${this.baseUrl}/forecast?q=${encodeURIComponent(city)}&appid=${this.apiKey}&units=metric&cnt=${days * 8}`; try { const response = await fetch(url); if (!response.ok) { throw new Error(`Forecast API error: ${response.status}`); } const data = await response.json(); return data.list.map(item => ({ date: new Date(item.dt * 1000), temperature: item.main.temp, description: item.weather[0].description })); } catch (error) { console.error('Failed to fetch forecast data:', error); throw error; } } } // Usage const weatherService = new WeatherService('your-api-key'); async function displayWeather() { try { const weather = await weatherService.getCurrentWeather('London'); console.log(`Current weather in ${weather.city}: ${weather.temperature}°C, ${weather.description}`); } catch (error) { console.error('Error displaying weather:', error.message); } } ``` REST API Client ```javascript class RESTClient { constructor(baseURL, defaultHeaders = {}) { this.baseURL = baseURL.replace(/\/$/, ''); // Remove trailing slash this.defaultHeaders = { 'Content-Type': 'application/json', ...defaultHeaders }; } async request(endpoint, options = {}) { const url = `${this.baseURL}/${endpoint.replace(/^\//, '')}`; const config = { headers: { ...this.defaultHeaders, ...options.headers }, ...options }; if (config.body && typeof config.body === 'object') { config.body = JSON.stringify(config.body); } try { const response = await fetch(url, config); if (!response.ok) { const errorData = await response.text(); throw new Error(`HTTP ${response.status}: ${errorData}`); } const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('application/json')) { return await response.json(); } return await response.text(); } catch (error) { console.error(`Request failed: ${config.method || 'GET'} ${url}`, error); throw error; } } get(endpoint, headers = {}) { return this.request(endpoint, { method: 'GET', headers }); } post(endpoint, body, headers = {}) { return this.request(endpoint, { method: 'POST', body, headers }); } put(endpoint, body, headers = {}) { return this.request(endpoint, { method: 'PUT', body, headers }); } delete(endpoint, headers = {}) { return this.request(endpoint, { method: 'DELETE', headers }); } } // Usage const api = new RESTClient('https://jsonplaceholder.typicode.com'); // GET request api.get('posts/1') .then(post => console.log('Post:', post)) .catch(error => console.error('Error:', error)); // POST request api.post('posts', { title: 'New Post', body: 'This is the content of the new post', userId: 1 }) .then(result => console.log('Created:', result)) .catch(error => console.error('Error:', error)); ``` Common Issues and Troubleshooting CORS Issues Cross-Origin Resource Sharing (CORS) is a common issue when working with APIs: ```javascript // This might fail due to CORS policy fetch('https://api.external-service.com/data') .then(response => response.json()) .catch(error => { if (error.message.includes('CORS')) { console.error('CORS error - the API does not allow requests from this domain'); // Consider using a proxy server or CORS proxy service } }); // Solution 1: Use a CORS proxy (for development only) const proxyUrl = 'https://cors-anywhere.herokuapp.com/'; const targetUrl = 'https://api.external-service.com/data'; fetch(proxyUrl + targetUrl) .then(response => response.json()) .then(data => console.log(data)) .catch(error => console.error('Error:', error)); // Solution 2: Configure your server to handle CORS // This is the preferred production solution ``` JSON Parsing Errors ```javascript const safeJsonParse = async (response) => { const text = await response.text(); if (!text) { return null; } try { return JSON.parse(text); } catch (error) { console.error('Invalid JSON response:', text); throw new Error('Response is not valid JSON'); } }; // Usage fetch('https://api.example.com/data') .then(safeJsonParse) .then(data => console.log(data)) .catch(error => console.error('Error:', error)); ``` Network Connectivity Issues ```javascript const checkNetworkAndFetch = async (url, options = {}) => { // Check if online if (!navigator.onLine) { throw new Error('No internet connection'); } try { const response = await fetch(url, options); return response; } catch (error) { if (error instanceof TypeError) { // Network error throw new Error('Network error - please check your connection'); } throw error; } }; // Listen for online/offline events window.addEventListener('online', () => { console.log('Connection restored'); // Retry failed requests }); window.addEventListener('offline', () => { console.log('Connection lost'); // Handle offline state }); ``` Memory Leaks with Large Responses ```javascript const fetchLargeData = async (url) => { const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } // For large responses, consider streaming const reader = response.body.getReader(); const chunks = []; try { while (true) { const { done, value } = await reader.read(); if (done) break; chunks.push(value); } // Process chunks as needed const fullData = new Uint8Array(chunks.reduce((acc, chunk) => acc + chunk.length, 0)); let offset = 0; for (const chunk of chunks) { fullData.set(chunk, offset); offset += chunk.length; } return fullData; } finally { reader.releaseLock(); } }; ``` Best Practices and Professional Tips 1. Create Reusable API Functions ```javascript // Create a configuration object for your API const API_CONFIG = { baseURL: process.env.API_BASE_URL || 'https://api.example.com', timeout: 10000, retries: 3 }; // Create utility functions const apiUtils = { buildUrl: (endpoint, params = {}) => { const url = new URL(endpoint, API_CONFIG.baseURL); Object.keys(params).forEach(key => { if (params[key] !== null && params[key] !== undefined) { url.searchParams.append(key, params[key]); } }); return url.toString(); }, getAuthHeaders: () => { const token = localStorage.getItem('authToken'); return token ? { 'Authorization': `Bearer ${token}` } : {}; } }; ``` 2. Implement Request/Response Interceptors ```javascript class APIInterceptor { constructor() { this.requestInterceptors = []; this.responseInterceptors = []; } addRequestInterceptor(interceptor) { this.requestInterceptors.push(interceptor); } addResponseInterceptor(interceptor) { this.responseInterceptors.push(interceptor); } async fetch(url, options = {}) { // Apply request interceptors let finalOptions = options; for (const interceptor of this.requestInterceptors) { finalOptions = await interceptor(url, finalOptions); } let response = await fetch(url, finalOptions); // Apply response interceptors for (const interceptor of this.responseInterceptors) { response = await interceptor(response); } return response; } } // Usage const api = new APIInterceptor(); // Add logging interceptor api.addRequestInterceptor(async (url, options) => { console.log(`Making request to: ${url}`); return options; }); // Add auth token interceptor api.addRequestInterceptor(async (url, options) => { const token = localStorage.getItem('authToken'); if (token) { options.headers = { ...options.headers, 'Authorization': `Bearer ${token}` }; } return options; }); ``` 3. Use Environment-Specific Configuration ```javascript const getApiConfig = () => { const env = process.env.NODE_ENV || 'development'; const configs = { development: { baseURL: 'http://localhost:3000/api', timeout: 5000, debug: true }, staging: { baseURL: 'https://staging-api.example.com', timeout: 8000, debug: false }, production: { baseURL: 'https://api.example.com', timeout: 10000, debug: false } }; return configs[env] || configs.development; }; const config = getApiConfig(); ``` 4. Implement Caching Strategies ```javascript class CachedAPIClient { constructor(cacheTimeout = 5 60 1000) { // 5 minutes default this.cache = new Map(); this.cacheTimeout = cacheTimeout; } generateCacheKey(url, options) { return `${url}_${JSON.stringify(options)}`; } async fetch(url, options = {}) { const cacheKey = this.generateCacheKey(url, options); const cached = this.cache.get(cacheKey); // Check if cached data is still valid if (cached && Date.now() - cached.timestamp < this.cacheTimeout) { console.log('Returning cached data for:', url); return cached.data; } try { const response = await fetch(url, options); const data = await response.json(); // Cache the response this.cache.set(cacheKey, { data, timestamp: Date.now() }); return data; } catch (error) { // If request fails, return cached data if available if (cached) { console.log('Request failed, returning stale cached data'); return cached.data; } throw error; } } clearCache() { this.cache.clear(); } } ``` 5. Handle Rate Limiting ```javascript class RateLimitedClient { constructor(requestsPerSecond = 10) { this.requestsPerSecond = requestsPerSecond; this.requestQueue = []; this.isProcessing = false; } async fetch(url, options = {}) { return new Promise((resolve, reject) => { this.requestQueue.push({ url, options, resolve, reject }); this.processQueue(); }); } async processQueue() { if (this.isProcessing || this.requestQueue.length === 0) { return; } this.isProcessing = true; while (this.requestQueue.length > 0) { const { url, options, resolve, reject } = this.requestQueue.shift(); try { const response = await fetch(url, options); resolve(response); } catch (error) { reject(error); } // Wait before next request await new Promise(resolve => setTimeout(resolve, 1000 / this.requestsPerSecond) ); } this.isProcessing = false; } } ``` Performance Optimization 1. Use Request Deduplication ```javascript class DeduplicatedClient { constructor() { this.pendingRequests = new Map(); } async fetch(url, options = {}) { const key = `${url}_${JSON.stringify(options)}`; // If request is already pending, return the same Promise if (this.pendingRequests.has(key)) { console.log('Deduplicating request for:', url); return this.pendingRequests.get(key); } // Create new request and store it const promise = fetch(url, options) .then(response => { this.pendingRequests.delete(key); return response; }) .catch(error => { this.pendingRequests.delete(key); throw error; }); this.pendingRequests.set(key, promise); return promise; } } const client = new DeduplicatedClient(); // These will result in only one actual HTTP request Promise.all([ client.fetch('https://api.example.com/data'), client.fetch('https://api.example.com/data'), client.fetch('https://api.example.com/data') ]).then(responses => { console.log('All responses received'); }); ``` 2. Implement Connection Pooling ```javascript class ConnectionPool { constructor(maxConnections = 6) { this.maxConnections = maxConnections; this.activeConnections = 0; this.queue = []; } async fetch(url, options = {}) { return new Promise((resolve, reject) => { this.queue.push({ url, options, resolve, reject }); this.processQueue(); }); } async processQueue() { if (this.activeConnections >= this.maxConnections || this.queue.length === 0) { return; } const { url, options, resolve, reject } = this.queue.shift(); this.activeConnections++; try { const response = await fetch(url, options); resolve(response); } catch (error) { reject(error); } finally { this.activeConnections--; this.processQueue(); // Process next request in queue } } } ``` 3. Optimize for Mobile and Slow Networks ```javascript class NetworkAwareClient { constructor() { this.connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection; this.isSlowNetwork = this.checkSlowNetwork(); } checkSlowNetwork() { if (!this.connection) return false; const slowTypes = ['slow-2g', '2g']; return slowTypes.includes(this.connection.effectiveType) || this.connection.downlink < 1.5; } async fetch(url, options = {}) { const optimizedOptions = { ...options }; if (this.isSlowNetwork) { // Reduce timeout for slow networks optimizedOptions.timeout = 3000; // Add compression headers optimizedOptions.headers = { ...optimizedOptions.headers, 'Accept-Encoding': 'gzip, deflate, br' }; } return fetch(url, optimizedOptions); } // Monitor network changes startNetworkMonitoring() { if (this.connection) { this.connection.addEventListener('change', () => { this.isSlowNetwork = this.checkSlowNetwork(); console.log('Network condition changed:', this.connection.effectiveType); }); } } } ``` 4. Use Streaming for Large Responses ```javascript async function streamResponse(url) { const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let result = ''; try { while (true) { const { done, value } = await reader.read(); if (done) break; // Process chunk immediately const chunk = decoder.decode(value, { stream: true }); result += chunk; // You can process data as it arrives console.log('Received chunk:', chunk.length, 'bytes'); // Optional: Update UI progressively // updateUI(chunk); } return result; } finally { reader.releaseLock(); } } ``` 5. Implement Smart Retry with Circuit Breaker ```javascript class CircuitBreaker { constructor(failureThreshold = 5, timeout = 60000) { this.failureThreshold = failureThreshold; this.timeout = timeout; this.failureCount = 0; this.lastFailureTime = null; this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN } async fetch(url, options = {}) { if (this.state === 'OPEN') { if (Date.now() - this.lastFailureTime < this.timeout) { throw new Error('Circuit breaker is OPEN'); } this.state = 'HALF_OPEN'; } try { const response = await fetch(url, options); if (response.ok) { this.reset(); return response; } throw new Error(`HTTP error! status: ${response.status}`); } catch (error) { this.recordFailure(); throw error; } } recordFailure() { this.failureCount++; this.lastFailureTime = Date.now(); if (this.failureCount >= this.failureThreshold) { this.state = 'OPEN'; console.log('Circuit breaker opened due to failures'); } } reset() { this.failureCount = 0; this.state = 'CLOSED'; this.lastFailureTime = null; } } ``` Conclusion The `fetch()` API has revolutionized how we handle HTTP requests in JavaScript, providing a modern, promise-based approach that's both powerful and flexible. Throughout this comprehensive guide, we've explored everything from basic GET requests to advanced patterns like circuit breakers and connection pooling. Key Takeaways 1. Always Handle Errors Properly: Remember that `fetch()` only rejects for network errors, not HTTP status codes. Always check `response.ok` and implement comprehensive error handling. 2. Use Modern Async/Await Syntax: While `.then()` chains work, async/await provides cleaner, more readable code that's easier to debug and maintain. 3. Implement Retry Logic: Network requests can fail for various reasons. Implementing retry logic with exponential backoff makes your applications more resilient. 4. Consider Performance: Use techniques like request deduplication, caching, and streaming for better performance, especially on mobile devices and slow networks. 5. Security First: Always validate and sanitize data, use HTTPS, and implement proper authentication headers when working with sensitive APIs. 6. Plan for Scale: As your application grows, consider implementing patterns like circuit breakers, rate limiting, and connection pooling to handle increased load gracefully. Moving Forward The `fetch()` API continues to evolve, with new features and improvements being added regularly. Stay updated with the latest specifications and browser support to take advantage of new capabilities as they become available. Consider integrating `fetch()` with modern frameworks and libraries like React, Vue, or Angular, and explore how it works with state management solutions and testing frameworks to build robust, maintainable web applications. Remember that mastering the `fetch()` API is not just about knowing the syntax—it's about understanding HTTP fundamentals, handling edge cases gracefully, and building applications that provide excellent user experiences even when network conditions are challenging. By following the patterns and best practices outlined in this guide, you'll be well-equipped to handle any API integration challenge and build applications that are reliable, performant, and user-friendly.