Fetch API Fundamentals

Understanding modern browser-based network requests

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.

graph TD A[Client Application] -->|request| B[Fetch API] B -->|HTTP request| C[Server] C -->|HTTP response| B B -->|parsed data| A style A fill:#f9f,stroke:#333,stroke-width:2px style B fill:#bbf,stroke:#333,stroke-width:2px style C fill:#bfb,stroke:#333,stroke-width:2px

Evolution of Browser Network Requests

To appreciate Fetch, it's helpful to understand the evolution of browser-based network requests:

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

flowchart LR A[fetch call] --> B[Promise] B --> C[Response Object] C --> D1[response.json()] C --> D2[response.text()] C --> D3[response.blob()] D1 --> E1[JSON Data] D2 --> E2[Plain Text] D3 --> E3[Binary Data] style A fill:#e3f2fd,stroke:#333,stroke-width:1px style B fill:#e8f5e9,stroke:#333,stroke-width:1px style C fill:#fff3e0,stroke:#333,stroke-width:1px

Response Processing Methods

The Response object provides several methods to handle different types of data:

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

// 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:

graph TB subgraph Information[1xx Informational] A[100 Continue] B[101 Switching Protocols] C[103 Early Hints] end subgraph Success[2xx Success] D[200 OK] E[201 Created] F[204 No Content] G[206 Partial Content] end subgraph Redirection[3xx Redirection] H[301 Moved Permanently] I[302 Found] J[304 Not Modified] K[307 Temporary Redirect] end subgraph ClientErrors[4xx Client Errors] L[400 Bad Request] M[401 Unauthorized] N[403 Forbidden] O[404 Not Found] P[409 Conflict] Q[429 Too Many Requests] end subgraph ServerErrors[5xx Server Errors] R[500 Internal Server Error] S[502 Bad Gateway] T[503 Service Unavailable] U[504 Gateway Timeout] end style Information fill:#e3f2fd,stroke:#333,stroke-width:1px style Success fill:#e8f5e9,stroke:#333,stroke-width:1px style Redirection fill:#fff3e0,stroke:#333,stroke-width:1px style ClientErrors fill:#ffebee,stroke:#333,stroke-width:1px style ServerErrors fill:#ffcdd2,stroke:#333,stroke-width:1px

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:

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

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:

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.

Further Reading