How to Use the JS Fetch API Complete Tutorial

A complete tutorial on the JavaScript Fetch API. Covers GET and POST requests, response handling, JSON parsing, headers configuration, error handling, AbortController for cancellation, query parameters, timeout patterns, and migrating from XMLHttpRequest to fetch.

JavaScriptintermediate
13 min read

The Fetch API is the modern, Promise-based way to make HTTP requests in JavaScript. It replaces XMLHttpRequest with a cleaner interface that works naturally with async/await. Available in all modern browsers and Node.js 18+, Fetch is the standard tool for API communication.

Basic GET Request

javascriptjavascript
const response = await fetch("https://api.example.com/users");
const users = await response.json();
console.log(users);

fetch() returns a Promise that resolves to a Response object. The body must be read separately using .json(), .text(), .blob(), or .arrayBuffer().

Response Object

Property/MethodTypeDescription
response.okbooleantrue if status is 200-299
response.statusnumberHTTP status code (200, 404, etc.)
response.statusTextstringStatus message ("OK", "Not Found")
response.headersHeadersResponse headers object
response.json()PromiseParse body as JSON
response.text()PromiseParse body as text
response.blob()PromiseParse body as Blob
response.clone()ResponseClone for reading body twice

Error Handling

Fetch does not reject on HTTP errors (404, 500). It only rejects on network failures:

javascriptjavascript
async function fetchJSON(url) {
  const response = await fetch(url);
 
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
  }
 
  return response.json();
}
 
try {
  const data = await fetchJSON("/api/users/999");
  console.log(data);
} catch (error) {
  console.error("Request failed:", error.message);
  // "HTTP 404: Not Found" for missing resources
  // "Failed to fetch" for network errors
}

This is the most important Fetch gotcha: always check response.ok or response.status.

POST Request With JSON Body

javascriptjavascript
async function createUser(userData) {
  const response = await fetch("/api/users", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(userData),
  });
 
  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.message || `HTTP ${response.status}`);
  }
 
  return response.json();
}
 
const newUser = await createUser({
  name: "Alice",
  email: "alice@example.com",
});

See handling POST requests with JS fetch API guide for advanced POST patterns.

All HTTP Methods

javascriptjavascript
// PUT (full update)
await fetch("/api/users/42", {
  method: "PUT",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "Alice", email: "newemail@example.com" }),
});
 
// PATCH (partial update)
await fetch("/api/users/42", {
  method: "PATCH",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ email: "newemail@example.com" }),
});
 
// DELETE
await fetch("/api/users/42", {
  method: "DELETE",
});

Headers

javascriptjavascript
// Using Headers constructor
const headers = new Headers();
headers.append("Authorization", "Bearer token123");
headers.append("Accept", "application/json");
 
const response = await fetch("/api/protected", { headers });
 
// Using plain object (simpler)
const response2 = await fetch("/api/protected", {
  headers: {
    Authorization: "Bearer token123",
    Accept: "application/json",
  },
});
 
// Reading response headers
console.log(response.headers.get("Content-Type"));
console.log(response.headers.get("X-Request-Id"));

Query Parameters

Build URLs with URLSearchParams:

javascriptjavascript
const params = new URLSearchParams({
  q: "javascript",
  page: 2,
  sort: "date",
  limit: 20,
});
 
const response = await fetch(`/api/search?${params}`);
// Fetches: /api/search?q=javascript&page=2&sort=date&limit=20

AbortController (Cancellation)

Cancel in-flight requests with AbortController:

javascriptjavascript
const controller = new AbortController();
 
// Start the request
const fetchPromise = fetch("/api/large-data", {
  signal: controller.signal,
});
 
// Cancel after 5 seconds
setTimeout(() => controller.abort(), 5000);
 
try {
  const response = await fetchPromise;
  const data = await response.json();
} catch (error) {
  if (error.name === "AbortError") {
    console.log("Request was cancelled");
  } else {
    throw error;
  }
}

Timeout Helper

javascriptjavascript
async function fetchWithTimeout(url, options = {}, timeoutMs = 10000) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
 
  try {
    const response = await fetch(url, {
      ...options,
      signal: controller.signal,
    });
    return response;
  } finally {
    clearTimeout(timeoutId);
  }
}
 
// 3-second timeout
const response = await fetchWithTimeout("/api/data", {}, 3000);

Sending Form Data

javascriptjavascript
// URL-encoded form data
const formData = new URLSearchParams();
formData.append("username", "alice");
formData.append("password", "secret123");
 
await fetch("/api/login", {
  method: "POST",
  body: formData,
  // Content-Type is set automatically to application/x-www-form-urlencoded
});
 
// Multipart form data (for file uploads)
const multipart = new FormData();
multipart.append("name", "Alice");
multipart.append("avatar", fileInput.files[0]);
 
await fetch("/api/profile", {
  method: "POST",
  body: multipart,
  // Do NOT set Content-Type — browser sets it with boundary
});

See uploading files via JS fetch API complete guide for file upload patterns.

Parallel Requests

javascriptjavascript
// Fetch multiple endpoints concurrently
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()),
]);

Reusable Fetch Wrapper

javascriptjavascript
class ApiClient {
  constructor(baseUrl, defaultHeaders = {}) {
    this.baseUrl = baseUrl;
    this.defaultHeaders = {
      "Content-Type": "application/json",
      ...defaultHeaders,
    };
  }
 
  async request(path, options = {}) {
    const response = await fetch(`${this.baseUrl}${path}`, {
      ...options,
      headers: { ...this.defaultHeaders, ...options.headers },
    });
 
    if (!response.ok) {
      const body = await response.json().catch(() => null);
      throw new Error(body?.message || `HTTP ${response.status}`);
    }
 
    return response.json();
  }
 
  get(path)              { return this.request(path); }
  post(path, data)       { return this.request(path, { method: "POST", body: JSON.stringify(data) }); }
  put(path, data)        { return this.request(path, { method: "PUT", body: JSON.stringify(data) }); }
  patch(path, data)      { return this.request(path, { method: "PATCH", body: JSON.stringify(data) }); }
  delete(path)           { return this.request(path, { method: "DELETE" }); }
}
 
const api = new ApiClient("https://api.example.com", {
  Authorization: "Bearer token123",
});
 
const users = await api.get("/users");
const newUser = await api.post("/users", { name: "Alice" });
Rune AI

Rune AI

Key Insights

  • Fetch does not reject on HTTP errors: Only network failures cause rejection; always check response.ok or response.status before reading the body
  • Body methods can only be called once: Use response.clone() if you need to read the body multiple times
  • AbortController enables cancellation: Pass a signal to cancel requests on timeout, component unmount, or user action
  • Do not set Content-Type for FormData: The browser must set it automatically to include the multipart boundary string
  • Build a reusable wrapper: Centralize base URL, headers, error handling, and auth token management in a single ApiClient class
RunePowered by Rune AI

Frequently Asked Questions

Does fetch send cookies by default?

For same-origin requests, yes. For cross-origin requests, set `credentials: "include"` in the options to send cookies.

How do I download a file with fetch?

Use `response.blob()` to get the binary data, then create an object URL: `URL.createObjectURL(await response.blob())`.

Can I track upload progress with fetch?

Not directly. Fetch does not expose upload progress events. Use `XMLHttpRequest` or the newer `ReadableStream` approach for upload progress tracking.

Is fetch available in Node.js?

Yes, from Node.js 18+ natively (via `undici`). For older Node versions, use the `node-fetch` package.

How is fetch different from Axios?

Fetch is built-in (no dependency), but requires manual error checking (`response.ok`) and JSON parsing. Axios auto-rejects on HTTP errors and auto-parses JSON. See [how to use Axios in JavaScript complete guide](/tutorials/programming-languages/javascript/how-to-use-axios-in-javascript-complete-guide) for the Axios alternative.

Conclusion

The Fetch API is the standard for HTTP requests in modern JavaScript. Always check response.ok for error handling, use AbortController for timeouts and cancellation, and build a reusable wrapper class for consistent API communication. For POST-specific patterns, see handling POST requests with JS fetch API guide. For the async architecture that powers Fetch, see the JS event loop architecture complete guide.