Skip to content
JavaScript js async 4 min read

The Fetch API

The Fetch API is the modern, promise-based way to make HTTP requests from JavaScript. It replaces the clunky XMLHttpRequest with a clean interface that fits naturally into async/await code. Available in every modern browser and in Node.js since v18, fetch is the default tool for talking to REST APIs, loading data, and submitting forms — so understanding how it handles responses and errors is essential.

Making a request

Calling fetch(url) kicks off a network request and immediately returns a promise. That promise resolves to a Response object as soon as the server sends back headers — it does not wait for the full body to download.

const promise = fetch("https://api.github.com/users/octocat");
console.log(promise); // Promise { <pending> }

The most common pattern is to await it inside an async function:

async function getUser() {
  const response = await fetch("https://api.github.com/users/octocat");
  console.log(response.status); // 200
  console.log(response.ok);     // true
}

getUser();

Output:

200
true

Reading the response

A Response doesn’t contain the body data directly — you read it through a method that returns another promise. Pick the method that matches the content type:

MethodReturnsUse for
.json()parsed object/arrayJSON APIs
.text()stringHTML, plain text, CSV
.blob()Blobimages, files, binary data
.arrayBuffer()ArrayBufferlow-level binary
.formData()FormDataform submissions

Because each of these returns a promise, you await them too:

async function getUser() {
  const response = await fetch("https://api.github.com/users/octocat");
  const data = await response.json();
  console.log(`${data.name} has ${data.public_repos} public repos`);
}

getUser();

Output:

The Octocat has 8 public repos

A response body can only be read once. Calling response.json() after response.text() on the same response throws “body stream already read.” If you need the raw text and the parsed object, read the text once and JSON.parse it yourself.

Checking for errors

Here is the most important fetch gotcha: fetch only rejects on network failures — a dropped connection, DNS error, or CORS block. An HTTP error status like 404 or 500 still resolves the promise. You must inspect response.ok (true for status 200–299) yourself.

async function getUser(username) {
  const response = await fetch(`https://api.github.com/users/${username}`);

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

  return response.json();
}

try {
  const user = await getUser("this-user-does-not-exist-xyz");
  console.log(user.name);
} catch (err) {
  console.error(err.message);
}

Output:

HTTP 404 - Not Found

The try/catch block here catches both network errors (from fetch rejecting) and HTTP errors (from the throw we added). Wrapping that logic in a reusable helper keeps every call site clean.

Sending data with POST

To do anything beyond a GET, pass a second argument: an options object. The key fields are method, headers, and body. Remember that body must be a string for JSON — call JSON.stringify on your object and set the matching Content-Type header.

async function createPost() {
  const response = await fetch("https://jsonplaceholder.typicode.com/posts", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      title: "Learning fetch",
      body: "It returns a promise!",
      userId: 1,
    }),
  });

  const created = await response.json();
  console.log(`Created post #${created.id}`);
}

createPost();

Output:

Created post #101

Common options you can pass:

OptionDescription
methodHTTP verb: GET (default), POST, PUT, PATCH, DELETE
headersrequest headers as an object or Headers instance
bodypayload string, FormData, Blob, or URLSearchParams
signalan AbortSignal to cancel the request
credentialsomit, same-origin, or include for cookies
modecors, no-cors, or same-origin

Working with headers

You can read response headers through the response.headers object, which behaves like a Map:

const response = await fetch("https://api.github.com/users/octocat");
console.log(response.headers.get("content-type"));

Output:

application/json; charset=utf-8

A complete live example

This self-contained demo fetches a random activity suggestion from a public API, handles errors, and renders the result.

async function loadActivity() {
  const output = document.getElementById("output");
  try {
    const res = await fetch("https://www.boredapi.com/api/activity");
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    const data = await res.json();
    output.textContent = `Try this: ${data.activity}`;
  } catch (err) {
    output.textContent = `Request failed: ${err.message}`;
  }
}

loadActivity();

Best practices

  • Always check response.ok (or response.status) before reading the body — fetch will not throw on 404 or 500.
  • Wrap requests in try/catch so network failures and HTTP errors are handled in one place.
  • Read each response body only once; store the result if you need it twice.
  • Set Content-Type: application/json and JSON.stringify the body when sending JSON.
  • Centralize fetch logic in a small wrapper (base URL, default headers, error handling) instead of repeating it.
  • Use an AbortController to cancel slow or stale requests and avoid memory leaks.
  • Prefer async/await over chained .then() calls for readability.
Last updated June 1, 2026
Was this helpful?