Docs LogoDocs

Fetch API - Making HTTP Requests

Documentation for Fetch API - Making HTTP Requests.

Fetch API - Making HTTP Requests

What is the Fetch API?

Fetch API is a modern interface for making HTTP requests in JavaScript. It returns Promises and is cleaner than the older XMLHttpRequest.

Definition: The Fetch API provides a JavaScript interface for making HTTP requests and processing responses. It uses Promises, making it perfect for use with async/await syntax, and is the standard way to make network requests in modern web applications.

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

// With async/await (preferred)
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);
  }
}

Why Use Fetch?

BenefitDescription
Promise-basedWorks seamlessly with async/await
Clean syntaxMuch simpler than XMLHttpRequest
Built-inNo external libraries needed
Stream supportCan read response body as a stream
Modern featuresAbortController, custom headers, CORS support

Fetch vs XMLHttpRequest

FeatureFetch APIXMLHttpRequest
SyntaxPromise-basedCallback-based
ReadabilityCleanVerbose
Browser supportModern browsersAll browsers
CORSAutomaticManual setup
StreamingYesLimited
Recommended✅ Yes❌ Legacy only

Basic GET Request

// Simple GET
fetch("https://api.example.com/users")
  .then((response) => response.json())
  .then((users) => console.log(users));

// With async/await
async function getUsers() {
  const response = await fetch("https://api.example.com/users");
  const users = await response.json();
  return users;
}

// With error handling
async function getUsers() {
  try {
    const response = await fetch("https://api.example.com/users");

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

    const users = await response.json();
    return users;
  } catch (error) {
    console.error("Fetch error:", error);
    throw error;
  }
}

Response Object

The Response object contains information about the HTTP response.

async function checkResponse() {
  const response = await fetch("https://api.example.com/data");

  console.log(response.ok); // true if status 200-299
  console.log(response.status); // 200, 404, 500, etc.
  console.log(response.statusText); // 'OK', 'Not Found', etc.
  console.log(response.headers); // Headers object
  console.log(response.url); // Final URL (after redirects)
  console.log(response.type); // 'basic', 'cors', etc.
  console.log(response.redirected); // true if response is from redirect
}

Response Methods

MethodDescriptionReturns
.json()Parse as JSONPromise<object>
.text()Get as textPromise<string>
.blob()Get as BlobPromise<Blob>
.arrayBuffer()Get as ArrayBufferPromise<ArrayBuffer>
.formData()Get as FormDataPromise<FormData>
.clone()Clone responseResponse
// JSON response
const data = await response.json();

// Text response (HTML, plain text)
const text = await response.text();

// Blob (for images, files)
const blob = await response.blob();
const imageUrl = URL.createObjectURL(blob);

// Clone response (body can only be read once!)
const clone = response.clone();
const data1 = await response.json();
const data2 = await clone.json(); // Read again from clone

POST Request

// POST with JSON
async function createUser(userData) {
  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;
}

// Usage
const user = await createUser({
  name: "John Doe",
  email: "john@example.com",
});

Request Methods

MethodPurposeHas Body
GETRetrieve data❌ No
POSTCreate data✅ Yes
PUTUpdate (replace)✅ Yes
PATCHUpdate (partial)✅ Yes
DELETEDelete data❌ Usually no
// GET - retrieve data
fetch("/api/users");

// POST - create new resource
fetch("/api/users", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "John", email: "john@example.com" }),
});

// PUT - replace entire resource
fetch("/api/users/1", {
  method: "PUT",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "John", email: "john@example.com", age: 30 }),
});

// PATCH - update specific fields
fetch("/api/users/1", {
  method: "PATCH",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "New Name" }),
});

// DELETE - remove resource
fetch("/api/users/1", {
  method: "DELETE",
});

Request Headers

// Setting headers
async function fetchWithHeaders() {
  const response = await fetch("https://api.example.com/data", {
    headers: {
      "Content-Type": "application/json",
      Authorization: "Bearer token123",
      Accept: "application/json",
      "X-Custom-Header": "custom-value",
    },
  });

  return await response.json();
}

// Reading response headers
async function readHeaders() {
  const response = await fetch("/api/data");

  console.log(response.headers.get("Content-Type"));
  console.log(response.headers.get("X-Custom-Response"));

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

Error Handling

Important: Fetch only rejects on network errors, NOT on HTTP errors (404, 500, etc.). You must check response.ok manually.

// ❌ This won't catch 404 errors!
try {
  const response = await fetch("/api/data");
  const data = await response.json();
} catch (error) {
  console.error(error); // Only catches network errors!
}

// ✅ Proper error handling
async function fetchData() {
  try {
    const response = await fetch("/api/data");

    // Check if response is OK (status 200-299)
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    const data = await response.json();
    return data;
  } catch (error) {
    console.error("Fetch failed:", error);
    throw error;
  }
}

// Detailed error handling by status code
async function fetchWithDetailedErrors(url) {
  try {
    const response = await fetch(url);

    if (!response.ok) {
      switch (response.status) {
        case 400:
          throw new Error("Bad request - check your data");
        case 401:
          throw new Error("Unauthorized - please login");
        case 403:
          throw new Error("Forbidden - you don't have permission");
        case 404:
          throw new Error("Resource not found");
        case 500:
          throw new Error("Server error - try again later");
        default:
          throw new Error(`HTTP error! status: ${response.status}`);
      }
    }

    return await response.json();
  } catch (error) {
    console.error("Error:", error.message);
    throw error;
  }
}

Request Options

const options = {
  method: "POST", // GET, POST, PUT, PATCH, DELETE
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify(data), // Request body
  mode: "cors", // cors, no-cors, same-origin
  credentials: "include", // include, same-origin, omit
  cache: "no-cache", // default, no-cache, reload, force-cache
  redirect: "follow", // follow, manual, error
  referrerPolicy: "no-referrer",
  signal: controller.signal, // For aborting requests
};

fetch(url, options);

Timeout Implementation

Fetch doesn't have built-in timeout, but you can implement it using AbortController.

// Timeout with AbortController
async function fetchWithTimeout(url, timeout = 5000) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeout);

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

// Usage
try {
  const data = await fetchWithTimeout("/api/data", 3000);
} catch (error) {
  console.error(error.message); // 'Request timeout' if > 3s
}

Canceling Requests

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

fetch("/api/data", {
  signal: controller.signal,
})
  .then((response) => response.json())
  .then((data) => console.log(data))
  .catch((error) => {
    if (error.name === "AbortError") {
      console.log("Request canceled");
    }
  });

// Cancel the request
controller.abort();

// Practical: Cancel previous request on new search
let controller = null;

async function search(query) {
  // Cancel previous request
  if (controller) {
    controller.abort();
  }

  // Create new controller for this request
  controller = new AbortController();

  try {
    const response = await fetch(`/api/search?q=${query}`, {
      signal: controller.signal,
    });
    return await response.json();
  } catch (error) {
    if (error.name !== "AbortError") {
      throw error;
    }
    return null; // Request was canceled
  }
}

Uploading Files

// Upload single file
async function uploadFile(file) {
  const formData = new FormData();
  formData.append("file", file);

  const response = await fetch("/api/upload", {
    method: "POST",
    body: formData, // Don't set Content-Type header! Browser sets it automatically
  });

  return await response.json();
}

// Upload multiple files
async function uploadMultiple(files) {
  const formData = new FormData();

  for (const file of files) {
    formData.append("files", file);
  }

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

  return await response.json();
}

// Upload with additional data
async function uploadWithData(file, metadata) {
  const formData = new FormData();
  formData.append("file", file);
  formData.append("title", metadata.title);
  formData.append("description", metadata.description);

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

  return await response.json();
}

// File input usage
const fileInput = document.getElementById("fileInput");
fileInput.addEventListener("change", async (e) => {
  const file = e.target.files[0];
  const result = await uploadFile(file);
  console.log("Upload result:", result);
});

Interview Questions & Answers

Q1: What is the Fetch API and how does it differ from XMLHttpRequest?

The Fetch API is a modern JavaScript interface for making HTTP requests that was introduced to replace the older XMLHttpRequest. The key difference is that Fetch uses Promises, which makes it work seamlessly with async/await syntax, while XMLHttpRequest uses callbacks. This makes Fetch code much cleaner and easier to read. Fetch also has a simpler API with sensible defaults, automatic CORS handling, and built-in support for streaming responses. However, XMLHttpRequest still works in all browsers including very old ones, while Fetch requires a polyfill for older browsers. For any new project, Fetch is the recommended approach.


Q2: Why doesn't fetch() reject on HTTP errors like 404 or 500?

Fetch only rejects when there's a network failure, like when the server is unreachable or there's no internet connection. HTTP error responses like 404 Not Found or 500 Server Error are still valid HTTP responses - the request succeeded in reaching the server and getting a response back. This design gives you more control over how to handle different status codes. To check for HTTP errors, you need to examine the response.ok property, which is true for status codes 200-299 and false otherwise. If you need error behavior, you must manually check this and throw an error yourself.


Q3: How do you implement timeout with fetch()?

Fetch doesn't have a built-in timeout option, so you need to implement it using AbortController. First, create an AbortController instance and pass its signal property to the fetch options. Then use setTimeout to call controller.abort() after your desired timeout period. When the request is aborted, fetch will reject with an AbortError that you can catch and handle as a timeout. This same pattern also allows you to cancel requests when users navigate away or start a new search. The AbortController is a general-purpose way to cancel any operation that accepts an AbortSignal.


Q4: What's the difference between PUT and PATCH methods?

FeaturePUTPATCH
PurposeFull replacementPartial update
SendsComplete resourceOnly changed fields
IdempotentYesYes
ExampleReplace entire user objectUpdate only user's email

PUT replaces the entire resource with the new data you send, so you must include all fields. PATCH updates only the specific fields you send, leaving other fields unchanged. For example, to update a user's email, PUT would require sending the entire user object with all fields, while PATCH would only require sending the email field. Both are idempotent, meaning multiple identical requests have the same effect as a single request.

Practical Examples

// Example 1: Complete CRUD operations
const API_URL = "https://api.example.com";

async function getUsers() {
  const response = await fetch(`${API_URL}/users`);
  if (!response.ok) throw new Error("Failed to fetch users");
  return await response.json();
}

async function createUser(user) {
  const response = await fetch(`${API_URL}/users`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(user),
  });
  if (!response.ok) throw new Error("Failed to create user");
  return await response.json();
}

async function updateUser(id, updates) {
  const response = await fetch(`${API_URL}/users/${id}`, {
    method: "PATCH",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(updates),
  });
  if (!response.ok) throw new Error("Failed to update user");
  return await response.json();
}

async function deleteUser(id) {
  const response = await fetch(`${API_URL}/users/${id}`, {
    method: "DELETE",
  });
  if (!response.ok) throw new Error("Failed to delete user");
}

// Example 2: Retry logic
async function fetchWithRetry(url, options = {}, retries = 3) {
  for (let i = 0; i < retries; i++) {
    try {
      const response = await fetch(url, options);
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      return await response.json();
    } catch (error) {
      if (i === retries - 1) throw error;
      await new Promise((resolve) => setTimeout(resolve, 1000 * (i + 1)));
    }
  }
}

// Example 3: Parallel requests
async function getDashboard() {
  const [users, posts, comments] = await Promise.all([
    fetch("/api/users").then((r) => r.json()),
    fetch("/api/posts").then((r) => r.json()),
    fetch("/api/comments").then((r) => r.json()),
  ]);

  return { users, posts, comments };
}

// Example 4: Search with cancel previous
let searchController;

async function search(query) {
  // Cancel previous search
  if (searchController) {
    searchController.abort();
  }

  searchController = new AbortController();

  try {
    const response = await fetch(`/api/search?q=${query}`, {
      signal: searchController.signal,
    });

    if (!response.ok) throw new Error("Search failed");
    return await response.json();
  } catch (error) {
    if (error.name === "AbortError") {
      return null; // Search was canceled
    }
    throw error;
  }
}

// Example 5: Download with progress
async function downloadWithProgress(url, onProgress) {
  const response = await fetch(url);
  const contentLength = response.headers.get("Content-Length");
  const total = parseInt(contentLength, 10);
  let loaded = 0;

  const reader = response.body.getReader();
  const chunks = [];

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

    chunks.push(value);
    loaded += value.length;
    onProgress(Math.round((loaded / total) * 100));
  }

  return new Blob(chunks);
}
Last updated on July 15, 2026

On this page