JavaScript Fetch API Complete Guide

Master modern network requests: basic usage, request configuration, response handling, error handling, and advanced techniques

JavaScript Fetch API Complete Guide

The Fetch API is the modern standard for making network requests in JavaScript. This article covers Fetch API usage and best practices.

Basic Usage

Simple Requests

// Basic GET request
const response = await fetch('https://api.example.com/data');
const data = await response.json();

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

Request Methods

// GET request (default)
const getResponse = await fetch('/api/users');

// POST request
const postResponse = await fetch('/api/users', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ name: 'John', email: 'john@example.com' })
});

// PUT request
const putResponse = await fetch('/api/users/1', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ name: 'Jane' })
});

// DELETE request
const deleteResponse = await fetch('/api/users/1', {
  method: 'DELETE'
});

// PATCH request
const patchResponse = await fetch('/api/users/1', {
  method: 'PATCH',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ status: 'active' })
});

Request Configuration

Full Configuration Options

const response = await fetch(url, {
  // Request method
  method: 'POST',

  // Request headers
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer token123',
    'X-Custom-Header': 'value'
  },

  // Request body
  body: JSON.stringify(data),

  // Request mode
  mode: 'cors', // 'cors', 'no-cors', 'same-origin'

  // Credentials mode
  credentials: 'include', // 'omit', 'same-origin', 'include'

  // Cache mode
  cache: 'no-cache', // 'default', 'no-store', 'reload', 'no-cache', 'force-cache'

  // Redirect handling
  redirect: 'follow', // 'follow', 'error', 'manual'

  // Referrer policy
  referrerPolicy: 'no-referrer-when-downgrade',

  // Integrity check
  integrity: 'sha256-abc123...',

  // Keep alive
  keepalive: false,

  // Signal (for cancellation)
  signal: abortController.signal
});

Headers Operations

// Using Headers object
const headers = new Headers();
headers.append('Content-Type', 'application/json');
headers.append('Authorization', 'Bearer token');

// Or pass object directly
const headers2 = new Headers({
  'Content-Type': 'application/json',
  'Authorization': 'Bearer token'
});

// Methods
headers.set('X-Custom', 'value');  // Set (overwrite)
headers.append('Accept', 'application/json'); // Append
headers.delete('X-Custom'); // Delete
headers.get('Content-Type'); // Get
headers.has('Authorization'); // Check

// Iterate
for (const [key, value] of headers) {
  console.log(`${key}: ${value}`);
}

// Use in request
await fetch(url, { headers });

Request Body Formats

// JSON data
await fetch(url, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ key: 'value' })
});

// Form data
const formData = new FormData();
formData.append('name', 'John');
formData.append('file', fileInput.files[0]);

await fetch(url, {
  method: 'POST',
  body: formData // No need to set Content-Type, browser handles it
});

// URL encoded data
const params = new URLSearchParams();
params.append('name', 'John');
params.append('age', '25');

await fetch(url, {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: params
});

// Binary data
const buffer = new ArrayBuffer(8);
await fetch(url, {
  method: 'POST',
  headers: { 'Content-Type': 'application/octet-stream' },
  body: buffer
});

// Blob data
const blob = new Blob(['Hello'], { type: 'text/plain' });
await fetch(url, {
  method: 'POST',
  body: blob
});

Response Handling

Response Object

const response = await fetch(url);

// Response status
console.log(response.status);       // Status code: 200, 404, 500...
console.log(response.statusText);   // Status text: OK, Not Found...
console.log(response.ok);           // Success (200-299)

// Response headers
console.log(response.headers.get('Content-Type'));
console.log(response.headers.get('Content-Length'));

// Response URL (may change after redirect)
console.log(response.url);

// Response type
console.log(response.type); // 'basic', 'cors', 'error', 'opaque'

// Redirected
console.log(response.redirected);

Parsing Response Body

const response = await fetch(url);

// JSON
const json = await response.json();

// Text
const text = await response.text();

// Blob (binary)
const blob = await response.blob();

// ArrayBuffer
const buffer = await response.arrayBuffer();

// FormData
const formData = await response.formData();

// Note: Body can only be read once
// Clone for multiple reads
const clone = response.clone();
const json1 = await response.json();
const json2 = await clone.json(); // Use clone

Streaming Reads

// Read large files or streaming data
async function readStream(url) {
  const response = await fetch(url);
  const reader = response.body.getReader();
  const contentLength = +response.headers.get('Content-Length');

  let receivedLength = 0;
  const chunks = [];

  while (true) {
    const { done, value } = await reader.read();

    if (done) break;

    chunks.push(value);
    receivedLength += value.length;

    // Progress
    const progress = (receivedLength / contentLength * 100).toFixed(2);
    console.log(`Download progress: ${progress}%`);
  }

  // Merge data
  const allChunks = new Uint8Array(receivedLength);
  let position = 0;
  for (const chunk of chunks) {
    allChunks.set(chunk, position);
    position += chunk.length;
  }

  return allChunks;
}

// Read text stream
async function readTextStream(url) {
  const response = await fetch(url);
  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let result = '';

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    result += decoder.decode(value, { stream: true });
  }

  return result;
}

Error Handling

Complete Error Handling

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

    // Check HTTP status
    if (!response.ok) {
      // Try to parse error response
      let errorMessage = response.statusText;
      try {
        const errorData = await response.json();
        errorMessage = errorData.message || errorMessage;
      } catch {
        // Cannot parse JSON
      }

      throw new HttpError(response.status, errorMessage);
    }

    return response;
  } catch (error) {
    if (error instanceof HttpError) {
      throw error;
    }

    // Network error
    if (error.name === 'TypeError') {
      throw new NetworkError('Network connection failed');
    }

    // Request aborted
    if (error.name === 'AbortError') {
      throw new AbortError('Request was cancelled');
    }

    throw error;
  }
}

// Custom error classes
class HttpError extends Error {
  constructor(status, message) {
    super(message);
    this.name = 'HttpError';
    this.status = status;
  }
}

class NetworkError extends Error {
  constructor(message) {
    super(message);
    this.name = 'NetworkError';
  }
}

class AbortError extends Error {
  constructor(message) {
    super(message);
    this.name = 'AbortError';
  }
}

// Usage
try {
  const response = await fetchWithErrorHandling('/api/data');
  const data = await response.json();
} catch (error) {
  if (error instanceof HttpError) {
    if (error.status === 401) {
      redirectToLogin();
    } else if (error.status === 404) {
      showNotFound();
    } else {
      showError(`Server error: ${error.message}`);
    }
  } else if (error instanceof NetworkError) {
    showError('Network connection failed, please check your connection');
  } else if (error instanceof AbortError) {
    console.log('Request was cancelled');
  }
}

Advanced Features

Request Cancellation

// Using AbortController
const controller = new AbortController();
const signal = controller.signal;

// Make request
const fetchPromise = fetch(url, { signal });

// Cancel request
controller.abort();

// Timeout cancellation
async function fetchWithTimeout(url, options = {}, timeout = 5000) {
  const controller = new AbortController();

  const timeoutId = setTimeout(() => {
    controller.abort();
  }, timeout);

  try {
    const response = await fetch(url, {
      ...options,
      signal: controller.signal
    });
    clearTimeout(timeoutId);
    return response;
  } catch (error) {
    clearTimeout(timeoutId);
    if (error.name === 'AbortError') {
      throw new Error('Request timeout');
    }
    throw error;
  }
}

// Using AbortSignal.timeout() (newer API)
const response = await fetch(url, {
  signal: AbortSignal.timeout(5000)
});

Request Retry

async function fetchWithRetry(url, options = {}, maxRetries = 3) {
  let lastError;

  for (let i = 0; i < maxRetries; i++) {
    try {
      const response = await fetch(url, options);

      if (response.ok) {
        return response;
      }

      // Only retry on server errors
      if (response.status >= 500) {
        throw new Error(`Server error: ${response.status}`);
      }

      // Don't retry client errors
      return response;
    } catch (error) {
      lastError = error;

      // Don't wait on last attempt
      if (i < maxRetries - 1) {
        // Exponential backoff
        const delay = Math.pow(2, i) * 1000;
        await new Promise(r => setTimeout(r, delay));
      }
    }
  }

  throw lastError;
}

// Advanced retry strategy
async function fetchWithAdvancedRetry(url, options = {}) {
  const config = {
    maxRetries: 3,
    baseDelay: 1000,
    maxDelay: 10000,
    retryOn: [408, 429, 500, 502, 503, 504],
    ...options.retry
  };

  let attempt = 0;

  while (true) {
    try {
      const response = await fetch(url, options);

      if (response.ok || !config.retryOn.includes(response.status)) {
        return response;
      }

      throw new Error(`HTTP ${response.status}`);
    } catch (error) {
      attempt++;

      if (attempt >= config.maxRetries) {
        throw error;
      }

      const delay = Math.min(
        config.baseDelay * Math.pow(2, attempt - 1),
        config.maxDelay
      );

      await new Promise(r => setTimeout(r, delay));
    }
  }
}

Concurrency Control

// Concurrency limiter
class ConcurrencyLimiter {
  constructor(limit = 5) {
    this.limit = limit;
    this.running = 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.running >= this.limit || this.queue.length === 0) {
      return;
    }

    this.running++;
    const { url, options, resolve, reject } = this.queue.shift();

    try {
      const response = await fetch(url, options);
      resolve(response);
    } catch (error) {
      reject(error);
    } finally {
      this.running--;
      this.processQueue();
    }
  }
}

// Usage
const limiter = new ConcurrencyLimiter(3);

const urls = ['/api/1', '/api/2', '/api/3', '/api/4', '/api/5'];
const responses = await Promise.all(
  urls.map(url => limiter.fetch(url))
);

HTTP Client Wrapper

Complete Wrapper

class HttpClient {
  constructor(baseURL = '', defaultOptions = {}) {
    this.baseURL = baseURL;
    this.defaultOptions = {
      headers: {
        'Content-Type': 'application/json'
      },
      ...defaultOptions
    };
    this.interceptors = {
      request: [],
      response: []
    };
  }

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

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

  async request(endpoint, options = {}) {
    let url = this.baseURL + endpoint;
    let config = this.mergeOptions(options);

    // Execute request interceptors
    for (const interceptor of this.interceptors.request) {
      const result = await interceptor({ url, ...config });
      url = result.url || url;
      config = { ...config, ...result };
    }

    try {
      let response = await fetch(url, config);

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

      if (!response.ok) {
        throw new HttpError(response.status, response.statusText);
      }

      return response;
    } catch (error) {
      throw error;
    }
  }

  mergeOptions(options) {
    return {
      ...this.defaultOptions,
      ...options,
      headers: {
        ...this.defaultOptions.headers,
        ...options.headers
      }
    };
  }

  async get(endpoint, params = {}) {
    const queryString = new URLSearchParams(params).toString();
    const url = queryString ? `${endpoint}?${queryString}` : endpoint;
    const response = await this.request(url);
    return response.json();
  }

  async post(endpoint, data) {
    const response = await this.request(endpoint, {
      method: 'POST',
      body: JSON.stringify(data)
    });
    return response.json();
  }

  async put(endpoint, data) {
    const response = await this.request(endpoint, {
      method: 'PUT',
      body: JSON.stringify(data)
    });
    return response.json();
  }

  async delete(endpoint) {
    const response = await this.request(endpoint, {
      method: 'DELETE'
    });
    return response.json();
  }
}

// Usage
const api = new HttpClient('https://api.example.com');

// Add auth interceptor
api.addRequestInterceptor(async (config) => {
  const token = localStorage.getItem('token');
  if (token) {
    config.headers = {
      ...config.headers,
      'Authorization': `Bearer ${token}`
    };
  }
  return config;
});

// Add response interceptor
api.addResponseInterceptor(async (response) => {
  if (response.status === 401) {
    // Handle auth failure
    redirectToLogin();
  }
  return response;
});

// API calls
const users = await api.get('/users', { page: 1, limit: 10 });
const newUser = await api.post('/users', { name: 'John' });

Best Practices Summary

Fetch API Best Practices:
┌─────────────────────────────────────────────────────┐
│                                                     │
│   Error Handling                                    │
│   ├── Check response.ok status                     │
│   ├── Handle network errors                        │
│   ├── Implement request timeouts                   │
│   └── Use appropriate retry strategies             │
│                                                     │
│   Performance                                       │
│   ├── Use concurrency control                      │
│   ├── Use caching appropriately                    │
│   ├── Cancel requests to avoid waste               │
│   └── Stream processing for large data             │
│                                                     │
│   Security                                          │
│   ├── Configure CORS properly                      │
│   ├── Use HTTPS                                    │
│   ├── Validate response data                       │
│   └── Protect sensitive information                │
│                                                     │
└─────────────────────────────────────────────────────┘
ScenarioRecommended Config
Cross-originmode: ‘cors’, credentials: ‘include’
Form uploadDon’t set Content-Type, use FormData
Large filesStreaming + progress display
Sensitive requestscredentials: ‘same-origin’

Master the Fetch API to build modern network request solutions.