Async JavaScriptFeatured

JavaScript Fetch API: Modern HTTP Requests

Master the Fetch API for making HTTP requests in JavaScript. Learn GET, POST, error handling, headers, and advanced patterns.

By JavaScriptDoc Team
fetchapihttpajaxasync

JavaScript Fetch API: Modern HTTP Requests

The Fetch API provides a modern, powerful, and flexible way to make HTTP requests in JavaScript. It's the successor to XMLHttpRequest and uses Promises for better async handling.

Introduction to Fetch

The Fetch API is a modern interface for making HTTP requests that returns Promises and provides a more powerful and flexible feature set than XMLHttpRequest.

// Basic fetch request
fetch('https://api.example.com/data')
  .then((response) => response.json())
  .then((data) => console.log(data))
  .catch((error) => console.error('Error:', error));

// Using async/await
async function getData() {
  try {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error('Error:', error);
  }
}

Making GET Requests

Basic GET Request

// Simple GET request
fetch('https://jsonplaceholder.typicode.com/users')
  .then((response) => {
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    return response.json();
  })
  .then((users) => {
    console.log('Users:', users);
  })
  .catch((error) => {
    console.error('Fetch error:', error);
  });

// With async/await
async function getUsers() {
  const response = await fetch('https://jsonplaceholder.typicode.com/users');

  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }

  const users = await response.json();
  return users;
}

// With query parameters
const params = new URLSearchParams({
  page: 1,
  limit: 10,
  sort: 'name',
});

fetch(`https://api.example.com/users?${params}`)
  .then((response) => response.json())
  .then((data) => console.log(data));

GET Request with Headers

// Custom headers
fetch('https://api.example.com/protected-data', {
  method: 'GET', // Optional, GET is default
  headers: {
    Authorization: 'Bearer YOUR_TOKEN_HERE',
    Accept: 'application/json',
    'X-Custom-Header': 'CustomValue',
  },
})
  .then((response) => response.json())
  .then((data) => console.log(data));

// Dynamic headers
async function fetchWithAuth(url, token) {
  const headers = {
    'Content-Type': 'application/json',
  };

  if (token) {
    headers['Authorization'] = `Bearer ${token}`;
  }

  const response = await fetch(url, { headers });
  return response.json();
}

Making POST Requests

Sending JSON Data

// POST with JSON data
const userData = {
  name: 'John Doe',
  email: 'john@example.com',
  age: 30,
};

fetch('https://api.example.com/users', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(userData),
})
  .then((response) => response.json())
  .then((data) => {
    console.log('Success:', data);
  })
  .catch((error) => {
    console.error('Error:', error);
  });

// With async/await
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 result = await response.json();
    return result;
  } catch (error) {
    console.error('Error creating user:', error);
    throw error;
  }
}

Sending Form Data

// FormData for file uploads
const formData = new FormData();
formData.append('username', 'john_doe');
formData.append('email', 'john@example.com');
formData.append('avatar', fileInput.files[0]);

fetch('https://api.example.com/upload', {
  method: 'POST',
  body: formData, // No Content-Type header needed
})
  .then((response) => response.json())
  .then((result) => console.log('Upload successful:', result));

// URL-encoded form data
const urlEncodedData = new URLSearchParams();
urlEncodedData.append('username', 'john_doe');
urlEncodedData.append('password', 'secure_password');

fetch('https://api.example.com/login', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded',
  },
  body: urlEncodedData,
})
  .then((response) => response.json())
  .then((data) => console.log(data));

Other HTTP Methods

PUT Request

// Update resource
const updatedData = {
  id: 1,
  name: 'Jane Doe',
  email: 'jane@example.com',
};

fetch('https://api.example.com/users/1', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(updatedData),
})
  .then((response) => response.json())
  .then((data) => console.log('Updated:', data));

// PATCH for partial updates
fetch('https://api.example.com/users/1', {
  method: 'PATCH',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ name: 'Jane Smith' }),
})
  .then((response) => response.json())
  .then((data) => console.log('Patched:', data));

DELETE Request

// Delete resource
fetch('https://api.example.com/users/1', {
  method: 'DELETE',
  headers: {
    Authorization: 'Bearer TOKEN',
  },
})
  .then((response) => {
    if (response.ok) {
      console.log('Deleted successfully');
    } else {
      throw new Error('Delete failed');
    }
  })
  .catch((error) => console.error('Error:', error));

// DELETE with confirmation
async function deleteUser(userId) {
  if (!confirm('Are you sure you want to delete this user?')) {
    return;
  }

  try {
    const response = await fetch(`https://api.example.com/users/${userId}`, {
      method: 'DELETE',
      headers: {
        Authorization: `Bearer ${getAuthToken()}`,
      },
    });

    if (!response.ok) {
      throw new Error('Delete failed');
    }

    return true;
  } catch (error) {
    console.error('Delete error:', error);
    return false;
  }
}

Handling Responses

Response Object

fetch('https://api.example.com/data')
  .then((response) => {
    // Response properties
    console.log('Status:', response.status);
    console.log('Status Text:', response.statusText);
    console.log('OK:', response.ok); // true if status 200-299
    console.log('Headers:', response.headers);
    console.log('URL:', response.url);
    console.log('Type:', response.type);

    // Get specific header
    console.log('Content-Type:', response.headers.get('Content-Type'));

    // Check status
    if (response.ok) {
      return response.json();
    } else if (response.status === 404) {
      throw new Error('Not found');
    } else {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
  })
  .then((data) => console.log(data))
  .catch((error) => console.error(error));

Different Response Types

// JSON response
fetch('/api/data')
  .then((response) => response.json())
  .then((data) => console.log(data));

// Text response
fetch('/api/text')
  .then((response) => response.text())
  .then((text) => console.log(text));

// Blob response (for files)
fetch('/api/image')
  .then((response) => response.blob())
  .then((blob) => {
    const url = URL.createObjectURL(blob);
    const img = document.createElement('img');
    img.src = url;
    document.body.appendChild(img);
  });

// ArrayBuffer response
fetch('/api/binary')
  .then((response) => response.arrayBuffer())
  .then((buffer) => {
    const view = new DataView(buffer);
    console.log(view);
  });

// FormData response
fetch('/api/form-data')
  .then((response) => response.formData())
  .then((formData) => {
    for (let [key, value] of formData) {
      console.log(`${key}: ${value}`);
    }
  });

Error Handling

Comprehensive Error Handling

class FetchError extends Error {
  constructor(response) {
    super(`HTTP Error ${response.status}: ${response.statusText}`);
    this.name = 'FetchError';
    this.response = response;
  }
}

async function fetchWithErrorHandling(url, options = {}) {
  try {
    const response = await fetch(url, options);

    if (!response.ok) {
      throw new FetchError(response);
    }

    const contentType = response.headers.get('content-type');

    if (contentType && contentType.includes('application/json')) {
      return await response.json();
    } else if (contentType && contentType.includes('text')) {
      return await response.text();
    } else {
      return response;
    }
  } catch (error) {
    if (error instanceof FetchError) {
      // Handle HTTP errors
      console.error('HTTP Error:', error.message);

      // Try to get error details from response body
      try {
        const errorData = await error.response.json();
        console.error('Error details:', errorData);
      } catch (e) {
        // Response body is not JSON
      }
    } else if (error instanceof TypeError) {
      // Network error or CORS issue
      console.error('Network error:', error.message);
    } else {
      // Other errors
      console.error('Unexpected error:', error);
    }

    throw error;
  }
}

Retry Logic

async function fetchWithRetry(url, options = {}, retries = 3, delay = 1000) {
  for (let i = 0; i < retries; i++) {
    try {
      const response = await fetch(url, options);

      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }

      return await response.json();
    } catch (error) {
      console.error(`Attempt ${i + 1} failed:`, error);

      if (i === retries - 1) {
        throw error;
      }

      // Exponential backoff
      await new Promise((resolve) =>
        setTimeout(resolve, delay * Math.pow(2, i))
      );
    }
  }
}

// Usage
fetchWithRetry('https://api.example.com/data', {}, 3, 1000)
  .then((data) => console.log('Success:', data))
  .catch((error) => console.error('Failed after retries:', error));

Advanced Fetch Patterns

Request Interceptor

class FetchInterceptor {
  constructor() {
    this.interceptors = {
      request: [],
      response: [],
    };
  }

  addRequestInterceptor(interceptor) {
    this.interceptors.request.push(interceptor);
  }

  addResponseInterceptor(interceptor) {
    this.interceptors.response.push(interceptor);
  }

  async fetch(url, options = {}) {
    // Apply request interceptors
    let modifiedOptions = { ...options };

    for (const interceptor of this.interceptors.request) {
      modifiedOptions = await interceptor(url, modifiedOptions);
    }

    // Make the request
    let response = await fetch(url, modifiedOptions);

    // Apply response interceptors
    for (const interceptor of this.interceptors.response) {
      response = await interceptor(response);
    }

    return response;
  }
}

// Usage
const api = new FetchInterceptor();

// Add auth token to all requests
api.addRequestInterceptor(async (url, options) => {
  const token = localStorage.getItem('auth_token');

  if (token) {
    options.headers = {
      ...options.headers,
      Authorization: `Bearer ${token}`,
    };
  }

  return options;
});

// Log all responses
api.addResponseInterceptor(async (response) => {
  console.log(`Response from ${response.url}: ${response.status}`);
  return response;
});

// Handle 401 responses
api.addResponseInterceptor(async (response) => {
  if (response.status === 401) {
    // Redirect to login
    window.location.href = '/login';
  }
  return response;
});

Request Cancellation

// Using AbortController
class CancellableFetch {
  constructor() {
    this.controllers = new Map();
  }

  async fetch(id, url, options = {}) {
    // Cancel any existing request with same ID
    this.cancel(id);

    // Create new controller
    const controller = new AbortController();
    this.controllers.set(id, controller);

    try {
      const response = await fetch(url, {
        ...options,
        signal: controller.signal,
      });

      this.controllers.delete(id);
      return response;
    } catch (error) {
      if (error.name === 'AbortError') {
        console.log(`Request ${id} was cancelled`);
      } else {
        throw error;
      }
    }
  }

  cancel(id) {
    const controller = this.controllers.get(id);
    if (controller) {
      controller.abort();
      this.controllers.delete(id);
    }
  }

  cancelAll() {
    for (const controller of this.controllers.values()) {
      controller.abort();
    }
    this.controllers.clear();
  }
}

// Usage
const api = new CancellableFetch();

// Start a request
api
  .fetch('user-search', '/api/users?q=john')
  .then((response) => response.json())
  .then((users) => console.log(users));

// Cancel it if user types again
api.cancel('user-search');

Parallel Requests

// Fetch multiple resources in parallel
async function fetchMultiple(urls) {
  const promises = urls.map((url) => fetch(url).then((r) => r.json()));

  try {
    const results = await Promise.all(promises);
    return results;
  } catch (error) {
    console.error('One or more requests failed:', error);
    throw error;
  }
}

// With individual error handling
async function fetchMultipleWithErrorHandling(urls) {
  const promises = urls.map(async (url) => {
    try {
      const response = await fetch(url);
      return {
        url,
        status: 'success',
        data: await response.json(),
      };
    } catch (error) {
      return {
        url,
        status: 'error',
        error: error.message,
      };
    }
  });

  return Promise.all(promises);
}

// Fetch with concurrency limit
async function fetchWithConcurrencyLimit(urls, limit = 3) {
  const results = [];
  const executing = [];

  for (const url of urls) {
    const promise = fetch(url).then((r) => r.json());
    results.push(promise);

    if (urls.length >= limit) {
      executing.push(promise);

      if (executing.length >= limit) {
        await Promise.race(executing);
        executing.splice(
          executing.findIndex((p) => p === promise),
          1
        );
      }
    }
  }

  return Promise.all(results);
}

Request/Response Caching

class FetchCache {
  constructor(ttl = 5 * 60 * 1000) {
    // 5 minutes default
    this.cache = new Map();
    this.ttl = ttl;
  }

  getCacheKey(url, options = {}) {
    return `${url}:${JSON.stringify(options)}`;
  }

  async fetch(url, options = {}) {
    const key = this.getCacheKey(url, options);

    // Check cache
    const cached = this.cache.get(key);
    if (cached && Date.now() - cached.timestamp < this.ttl) {
      console.log('Returning cached response for:', url);
      return cached.response.clone();
    }

    // Make fresh request
    const response = await fetch(url, options);

    // Cache successful responses
    if (response.ok) {
      this.cache.set(key, {
        response: response.clone(),
        timestamp: Date.now(),
      });
    }

    return response;
  }

  clear() {
    this.cache.clear();
  }

  remove(url, options = {}) {
    const key = this.getCacheKey(url, options);
    this.cache.delete(key);
  }
}

// Usage with cache
const cachedFetch = new FetchCache();

// First call - makes network request
await cachedFetch.fetch('/api/users');

// Second call within TTL - returns cached
await cachedFetch.fetch('/api/users');

Real-World Examples

API Client Class

class APIClient {
  constructor(baseURL, options = {}) {
    this.baseURL = baseURL;
    this.headers = options.headers || {};
    this.timeout = options.timeout || 30000;
  }

  async request(endpoint, options = {}) {
    const url = `${this.baseURL}${endpoint}`;
    const config = {
      ...options,
      headers: {
        'Content-Type': 'application/json',
        ...this.headers,
        ...options.headers,
      },
    };

    // Add timeout
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), this.timeout);

    try {
      const response = await fetch(url, {
        ...config,
        signal: controller.signal,
      });

      clearTimeout(timeoutId);

      if (!response.ok) {
        const error = await response.json().catch(() => ({}));
        throw new Error(error.message || `HTTP ${response.status}`);
      }

      return await response.json();
    } catch (error) {
      if (error.name === 'AbortError') {
        throw new Error('Request timeout');
      }
      throw error;
    }
  }

  get(endpoint, options = {}) {
    return this.request(endpoint, { ...options, method: 'GET' });
  }

  post(endpoint, data, options = {}) {
    return this.request(endpoint, {
      ...options,
      method: 'POST',
      body: JSON.stringify(data),
    });
  }

  put(endpoint, data, options = {}) {
    return this.request(endpoint, {
      ...options,
      method: 'PUT',
      body: JSON.stringify(data),
    });
  }

  delete(endpoint, options = {}) {
    return this.request(endpoint, { ...options, method: 'DELETE' });
  }

  setAuthToken(token) {
    if (token) {
      this.headers['Authorization'] = `Bearer ${token}`;
    } else {
      delete this.headers['Authorization'];
    }
  }
}

// Usage
const api = new APIClient('https://api.example.com', {
  timeout: 10000,
});

api.setAuthToken('your-token-here');

// GET request
const users = await api.get('/users');

// POST request
const newUser = await api.post('/users', {
  name: 'John Doe',
  email: 'john@example.com',
});

File Upload with Progress

function uploadFileWithProgress(file, url, onProgress) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    const formData = new FormData();
    formData.append('file', file);

    // Progress tracking
    xhr.upload.addEventListener('progress', (e) => {
      if (e.lengthComputable) {
        const percentComplete = (e.loaded / e.total) * 100;
        onProgress(percentComplete);
      }
    });

    xhr.addEventListener('load', () => {
      if (xhr.status >= 200 && xhr.status < 300) {
        resolve(JSON.parse(xhr.responseText));
      } else {
        reject(new Error(`Upload failed: ${xhr.status}`));
      }
    });

    xhr.addEventListener('error', () => {
      reject(new Error('Upload failed'));
    });

    xhr.open('POST', url);
    xhr.send(formData);
  });
}

// Fetch doesn't support upload progress, so we can combine approaches
class UploadManager {
  async uploadWithFetch(file, url) {
    const formData = new FormData();
    formData.append('file', file);

    const response = await fetch(url, {
      method: 'POST',
      body: formData,
    });

    if (!response.ok) {
      throw new Error('Upload failed');
    }

    return response.json();
  }

  async uploadMultiple(files, url, onProgress) {
    const total = files.length;
    let completed = 0;

    const promises = files.map(async (file, index) => {
      const result = await this.uploadWithFetch(file, url);
      completed++;
      onProgress((completed / total) * 100, index);
      return result;
    });

    return Promise.all(promises);
  }
}

Best Practices

  1. Always check response.ok

    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    
  2. Set appropriate headers

    fetch(url, {
      headers: {
        'Content-Type': 'application/json',
        Accept: 'application/json',
      },
    });
    
  3. Handle network errors

    try {
      const response = await fetch(url);
      // Handle response
    } catch (error) {
      if (error instanceof TypeError) {
        console.error('Network error:', error);
      }
    }
    
  4. Use AbortController for cancellation

    const controller = new AbortController();
    
    fetch(url, { signal: controller.signal }).catch((err) => {
      if (err.name === 'AbortError') {
        console.log('Fetch aborted');
      }
    });
    
    // Cancel the request
    controller.abort();
    

Conclusion

The Fetch API is a powerful tool for making HTTP requests in modern JavaScript:

  • Promise-based for better async handling
  • Flexible request configuration
  • Multiple response types support
  • Better error handling than XMLHttpRequest
  • Streaming capabilities for large responses
  • AbortController for cancellation

Key takeaways:

  • Always handle both network and HTTP errors
  • Use appropriate headers for content types
  • Implement retry logic for resilience
  • Consider caching for performance
  • Use AbortController for cancellable requests
  • Build reusable API clients for consistency

Master the Fetch API to build robust, modern web applications with efficient data fetching!