jsguides

fetch()

fetch(input[, init])

The fetch() function is the modern, Promise-based way to make HTTP requests in JavaScript. It replaces the older XMLHttpRequest API with a cleaner, more powerful interface built on top of Promises.

Unlike XMLHttpRequest, fetch() automatically rejects the Promise on network failure, making error handling more intuitive. However, it does not reject on HTTP error statuses (4xx, 5xx)—you must check response.ok manually.

Syntax

fetch(url)
fetch(url, options)

Parameters

ParameterTypeDefaultDescription
urlstring or RequestThe resource to fetch (URL string or Request object)
initRequestInitundefinedOptional configuration object containing method, headers, body, etc.

RequestInit Options

OptionTypeDescription
methodstringHTTP method (GET, POST, PUT, DELETE, etc.). Default: GET
headersHeaders or objectRequest headers
bodystring or FormData or Blob or URLSearchParamsRequest body (cannot be used with GET or HEAD)
modestringCORS mode: “cors”, “no-cors”, or “same-origin”
credentialsstringInclude cookies: “omit”, “same-origin”, or “include”
cachestringCache mode: “default”, “no-store”, “reload”, “no-cache”, “force-cache”, “only-if-cached”
redirectstringFollow, error, or manual redirect behavior
signalAbortSignalAbortController signal to cancel the request
referrerstringSet the request’s Referer header
referrerPolicystringReferrer policy

Examples

Basic GET request

A basic fetch() call with await reads JSON from a public API endpoint. The response.json() method parses the body into a JavaScript object. Because fetch() returns a Promise, you can use async/await for a linear-looking control flow.

async function fetchUser() {
  const response = await fetch('https://jsonplaceholder.typicode.com/users/1');
  const data = await response.json();
  console.log(data);
  // { id: 1, name: "Leanne Graham", email: "Sincere@april.biz", ... }
}

fetchUser();

GET requests don’t require an options object — fetch(url) defaults to GET. When you need to send data to the server, switch to a POST request with a body and the appropriate Content-Type header.

POST request with JSON body

For POST requests, provide a method, headers, and body in the options object. The Content-Type: application/json header tells the server how to interpret the payload, and JSON.stringify() converts the JavaScript object to a string before sending.

async function createPost() {
  const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      title: 'My New Post',
      body: 'This is the post content.',
      userId: 1
    })
  });
  
  const data = await response.json();
  console.log('Created:', data);
  // Created: { title: "My New Post", body: "This is the post content.", userId: 1, id: 101 }
}

createPost();

A successful POST doesn’t guarantee the server accepted your data — the response could still carry a 4xx or 5xx status. Since fetch() never rejects on HTTP errors, you must inspect response.ok or response.status yourself.

Handling errors and checking response.ok

A try/catch block wraps the fetch call so network failures trigger the catch clause, and response.ok guards against HTTP-level errors. This two-tier approach lets you distinguish between a failed network request and a server that returned a 404 or 500.

async function safeFetch(url) {
  try {
    const response = await fetch(url);
    
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    
    return await response.json();
  } catch (error) {
    console.error('Fetch failed:', error.message);
    throw error;
  }
}

safeFetch('https://jsonplaceholder.typicode.com/posts/1');
// { userId: 1, id: 1, title: "...", body: "..." }

safeFetch('https://jsonplaceholder.typicode.com/nonexistent');
// Fetch failed: HTTP error! status: 404

Sometimes a request hangs indefinitely — a slow server or a network stall can leave your app waiting. AbortController gives you a way to cancel an in-flight fetch() after a timeout, preventing hung requests from piling up.

Canceling a request with AbortController

Create an AbortController, pass its signal into the fetch options, and call controller.abort() to cancel. The fetch Promise rejects with an AbortError, which you can distinguish from other errors by checking err.name. Always clear the timeout in a finally block to avoid leaking timers.

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

fetchWithTimeout('https://jsonplaceholder.typicode.com/users/1', 1000)
  .then(data => console.log(data))
  .catch(err => console.error(err.name === 'AbortError' ? 'Request timed out' : err));

By default, fetch() does not include cookies in cross-origin requests. Setting credentials: 'include' changes that, which is necessary when your API relies on session cookies for authentication. The server must also respond with Access-Control-Allow-Credentials: true for the browser to accept the response.

Sending cookies with credentials

async function fetchWithCookies(url) {
  const response = await fetch(url, {
    credentials: 'include',
    headers: {
      'Accept': 'application/json'
    }
  });
  return response.json();
}

For same-origin requests, credentials are included by default. The same-origin value is the default for credentials, while omit never sends them even on the same origin. Choose include only when you need cross-origin cookie exchange.

Common Patterns

Real-world fetch() usage often combines several of the techniques above into reusable helpers. The patterns below show how to wrap fetch() for JSON APIs, retry logic, and parallel requests.

JSON helper function:

async function fetchJSON(url, options = {}) {
  const response = await fetch(url, {
    ...options,
    headers: {
      'Content-Type': 'application/json',
      ...options.headers
    }
  });
  
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
  }
  
  return response.json();
}

const user = await fetchJSON('https://api.example.com/user/1');

Wrapping fetch() in a helper centralizes error handling and default headers. A single JSON wrapper keeps your application code clean when every endpoint returns JSON. When network conditions are unreliable, you may want the request to retry rather than fail immediately.

Retry with exponential backoff:

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 && i < retries - 1) {
        await new Promise(r => setTimeout(r, delay * Math.pow(2, i)));
        continue;
      }
      return response;
    } catch (err) {
      if (i === retries - 1) throw err;
      await new Promise(r => setTimeout(r, delay * Math.pow(2, i)));
    }
  }
}

Exponential backoff increases the delay after each failed attempt — the delay * Math.pow(2, i) formula doubles the wait each time. This pattern avoids hammering a struggling server while still recovering automatically from transient failures. When you need data from several independent endpoints, retrying each sequentially would be slow; running them in parallel with Promise.all() is usually the better choice.

Parallel requests with Promise.all:

async function fetchMultiple(urls) {
  const requests = urls.map(url => fetch(url).then(r => r.json()));
  return Promise.all(requests);
}

const [users, posts] = await fetchMultiple([
  'https://jsonplaceholder.typicode.com/users',
  'https://jsonplaceholder.typicode.com/posts'
]);

Response body methods

A Response object exposes several methods for reading the response body, each returning a Promise that resolves to a different type:

  • response.json() — parses the body as JSON and returns a JavaScript value
  • response.text() — returns the body as a plain string
  • response.blob() — returns a Blob, useful for binary files and images
  • response.arrayBuffer() — returns raw binary data as an ArrayBuffer
  • response.formData() — returns a FormData object

Each body method can only be called once per response — calling response.json() twice on the same response throws because the stream has already been consumed. If you need to read the body multiple times, call response.clone() first.

fetch() vs XMLHttpRequest

fetch() replaced XMLHttpRequest as the standard way to make HTTP requests. The two main differences in behavior are: fetch() uses Promises instead of callbacks, and it does not automatically reject on HTTP error status codes (4xx, 5xx) — you must check response.ok or response.status yourself. XMLHttpRequest fired onerror only for network failures, while fetch() rejects its Promise on network failures but resolves it for HTTP errors. This design means you always need to inspect the response, which makes error handling explicit rather than implicit.

CORS and the mode option

By default, fetch() operates in "cors" mode, which means cross-origin requests must include appropriate Access-Control-Allow-Origin headers from the server. Setting mode: "no-cors" allows the request but returns an opaque response — the body is unreadable and the status is always 0. This is generally only useful for requests where you don’t need to read the response, such as sending analytics pings. For same-origin requests, "same-origin" mode explicitly prevents the request from going cross-origin.

See Also