2 min read
On this page

Fetch & HTTP Requests

fetch(): The Modern HTTP Client

fetch is the built-in browser API for HTTP requests. It returns a promise that resolves to a Response object. Available in all modern browsers and Node.js 18+.

const response = await fetch("/api/users");
const users = await response.json();

GET Requests

GET is the default method. Build query strings with URLSearchParams.

const params = new URLSearchParams({ page: 2, limit: 20, sort: "name" });
const response = await fetch(`/api/users?${params}`);
const users = await response.json();

POST with JSON Body

Set Content-Type and stringify the body.

const response = await fetch("/api/users", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "Alice", email: "alice@example.com" })
});
const newUser = await response.json();

Other Methods

await fetch(`/api/users/${id}`, {
  method: "PUT",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify(updatedUser)
});

await fetch(`/api/users/${id}`, {
  method: "PATCH",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ role: "admin" })
});

await fetch(`/api/users/${id}`, { method: "DELETE" });

Headers

const response = await fetch("/api/data", {
  headers: {
    "Content-Type": "application/json",
    "Authorization": `Bearer ${token}`,
    "Accept": "application/json"
  }
});

// Reading response headers
console.log(response.headers.get("Content-Type"));

Error Handling: fetch Does Not Reject on HTTP Errors

This is the most important thing about fetch. A 404 or 500 is not a rejection. The promise rejects only on network failures. Always check response.ok.

async function fetchJSON(url) {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
  }
  return response.json();
}

Comprehensive Error Handling

async function apiRequest(url, options = {}) {
  let response;
  try {
    response = await fetch(url, options);
  } catch (error) {
    throw new Error(`Network error: ${error.message}`);
  }

  if (!response.ok) {
    let body;
    try { body = await response.json(); } catch { body = {}; }
    const err = new Error(body.message || `HTTP ${response.status}`);
    err.status = response.status;
    throw err;
  }
  return response.json();
}

try {
  const user = await apiRequest("/api/users/42");
} catch (error) {
  if (error.status === 404) console.log("Not found");
  else if (error.status === 401) redirectToLogin();
  else console.error(error.message);
}

AbortController for Cancellation

Cancel in-flight requests. Essential for search-as-you-type, unmounting, and timeouts.

const controller = new AbortController();
const response = await fetch("/api/data", { signal: controller.signal });
controller.abort();  // cancels the request

Timeout Pattern

async function fetchWithTimeout(url, ms = 5000) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), ms);
  try {
    return await fetch(url, { signal: controller.signal });
  } finally {
    clearTimeout(timeoutId);
  }
}

Search-As-You-Type

let currentController = null;

async function search(query) {
  if (currentController) currentController.abort();
  currentController = new AbortController();

  try {
    const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
      signal: currentController.signal
    });
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    displayResults(await response.json());
  } catch (error) {
    if (error.name === "AbortError") return;  // expected
    displayError(error.message);
  }
}

document.querySelector("#search").addEventListener("input", e => search(e.target.value));

Response Body Methods

The body is a stream consumed once. Each method can only be called once.

await response.json();       // parse as JSON
await response.text();       // plain text
await response.blob();       // binary blob
await response.arrayBuffer(); // raw bytes
await response.formData();   // form data

Streaming Large Responses

async function downloadWithProgress(url) {
  const response = await fetch(url);
  const total = parseInt(response.headers.get("Content-Length"), 10);
  const reader = response.body.getReader();
  let received = 0;

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    received += value.length;
    console.log(`${Math.round((received / total) * 100)}%`);
  }
}

Sending Form Data & File Uploads

Do not set Content-Type for FormData — the browser sets it with the correct boundary.

const formData = new FormData(document.querySelector("#upload-form"));
formData.append("file", fileInput.files[0]);

const response = await fetch("/api/upload", {
  method: "POST",
  body: formData  // no Content-Type header
});

When to Use fetch vs a Library

fetch is enough for most applications. Consider a library when you need interceptors, automatic retry, or request deduplication. Every one of those features can be built as a thin wrapper around fetch. Start with fetch. Add a wrapper when patterns repeat.

Common Pitfalls

Not checking response.ok. fetch does not reject on 404 or 500.

Setting Content-Type for FormData. The browser must set it to include the multipart boundary.

Reading the body twice. The body is a stream. Clone the response first if needed: response.clone().

Not cancelling stale requests. Search-as-you-type and navigation changes need AbortController.

Forgetting credentials: "include" for cross-origin cookies.

await fetch("https://api.other.com/data", { credentials: "include" });

Not encoding query parameters. Use URLSearchParams or encodeURIComponent.

Key Takeaways

  • fetch is the built-in HTTP client. No libraries needed for most use cases.
  • Always check response.ok. fetch does not reject on HTTP error codes.
  • Set Content-Type: application/json and JSON.stringify() the body for JSON requests.
  • Use AbortController to cancel requests for search, timeouts, and cleanup.
  • Response bodies are streams: read once with .json(), .text(), or .blob().
  • Do not set Content-Type when sending FormData.