How to Use the JS Fetch API: Complete Tutorial

The Fetch API is the modern way to make HTTP requests in JavaScript. Learn GET, POST, handling JSON responses, setting headers, catching errors, and working with async/await.

8 min read

The JavaScript Fetch API is the built-in browser and Node.js interface for making HTTP requests. It replaces the older XMLHttpRequest with a cleaner, promise-based design.

You pass it a URL, and it returns a promise that resolves to a Response object.

javascriptjavascript
fetch("https://api.example.com/users/1")
  .then((response) => response.json())
  .then((user) => console.log(user.name))
  .catch((error) => console.error("Request failed:", error.message));

Every fetch call starts a network request and immediately returns a promise, without waiting for the response to arrive first. The first then receives the response headers, but the body is not available yet at that point. You call json, text, or another body method separately to actually read it.

The fetch request and response lifecycle

Fetch API request and response flow

The response resolves in two stages. First, the fetch promise resolves with the response headers as soon as the server responds, well before the full body has necessarily finished downloading. Then you call a body method to read and parse the body content, and both stages are asynchronous on their own, each with its own promise to await.

Making a GET request and reading JSON

Most fetch calls are GET requests that return JSON. This is the pattern you will use most often.

javascriptjavascript
async function getUser(id) {
  const response = await fetch(`https://api.example.com/users/${id}`);
 
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
  }
 
  return response.json();
}

Calling the function and logging the result shows the full round trip, from request to a plain object ready to use.

javascriptjavascript
getUser(1).then((user) => console.log(user.name));

Three details matter here. The ok property is true only for status codes 200 through 299, so you must check it yourself, and the json method returns a promise that needs an await or a then to get the parsed object.

If the response body is not valid JSON, that same method rejects with a SyntaxError instead of returning data.

The Response object: status, headers, and body

The Response object has several useful properties beyond just the body.

javascriptjavascript
async function inspectResponse(url) {
  const response = await fetch(url);
 
  console.log("Status:", response.status);
  console.log("OK:", response.ok);
  console.log("Status text:", response.statusText);
  console.log("Redirected:", response.redirected);
  console.log("URL:", response.url);
}

Headers are iterable too, so looping over response.headers with a for-of loop reads every header name and value the server sent back.

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

The table below lists the common ways to read the body, each returning a different parsed shape depending on what the response actually contains.

MethodReturnsUse for
jsonPromise of parsed JSONAPI responses
textPromise of stringHTML, plain text, CSV
blobPromise of BlobImages, files, binary data
arrayBufferPromise of ArrayBufferLow-level binary processing
formDataPromise of FormDataMultipart form responses

Each method can only be called once. After you read the body with json, calling text on that same response rejects instead of returning anything useful.

Making a POST request with JSON

To send data, pass an options object as the second argument to fetch, setting the method, headers, and body.

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

Calling that function and handling both outcomes looks the same as any other promise-based call, since createUser resolves with the created user or rejects with a readable error message.

javascriptjavascript
createUser("Taylor", "taylor@example.com")
  .then((user) => console.log("Created user with ID:", user.id))
  .catch((err) => console.error(err.message));

The body must be a string, FormData, Blob, URLSearchParams, or similar. You cannot pass a plain object directly, so use JSON.stringify() to convert one to a JSON string first. For a deeper dive into POST patterns including file uploads, see Handling Post Requests with JS Fetch API Guide.

Setting request headers

Headers control caching, authentication, content type, and more. Pass them as a plain object for a one-off request.

javascriptjavascript
const response = await fetch("https://api.example.com/data", {
  headers: {
    "Authorization": "Bearer my-token-here",
    "Accept": "application/json",
    "X-Request-ID": crypto.randomUUID(),
  },
});

Use a Headers instance instead when you need to append or modify headers programmatically before the request goes out, such as building up authentication headers conditionally based on whether a token exists.

javascriptjavascript
const headers = new Headers();
headers.set("Authorization", "Bearer my-token");
headers.append("Accept", "application/json");
 
const response = await fetch(url, { headers });

Handling errors correctly

Fetch only rejects on network failures: no internet, DNS failure, connection refused. An HTTP 404 or 500 resolves normally, so you must check the response's ok or status property yourself to detect server errors. A robust fetch wrapper handles three separate failure modes, starting with the network itself.

javascriptjavascript
async function requestOrThrow(url) {
  try {
    return await fetch(url);
  } catch (networkError) {
    console.error("Network error:", networkError.message);
    throw new Error("Unable to connect. Check your internet connection.");
  }
}

The second failure mode is a response that arrived but carries an error status, which fetch itself does not treat as a rejection.

javascriptjavascript
async function checkStatus(response) {
  if (!response.ok) {
    const body = await response.text().catch(() => "");
    console.error(`HTTP ${response.status}: ${body}`);
    throw new Error(`Server error: ${response.status}`);
  }
  return response;
}

The third is a response that reports success but whose body is not actually valid JSON, which the earlier two checks cannot catch since the request and the status both looked fine.

javascriptjavascript
async function safeFetch(url) {
  const response = await checkStatus(await requestOrThrow(url));
  try {
    return await response.json();
  } catch (parseError) {
    console.error("JSON parse error:", parseError.message);
    throw new Error("Received an invalid response from the server.");
  }
}

Splitting the network check, the status check, and the parse check into their own steps keeps each failure mode easy to test and reason about on its own. For more error handling patterns, see Handling Async Errors with Try Catch in JS Guide.

Fetch with query parameters

Use URLSearchParams to build query strings cleanly, since it handles encoding special characters automatically instead of requiring manual escaping.

javascriptjavascript
async function searchUsers(query, page = 1, limit = 10) {
  const params = new URLSearchParams({ q: query, page: String(page), limit: String(limit) });
  const response = await fetch(`https://api.example.com/users/search?${params}`);
 
  if (!response.ok) {
    throw new Error(`Search failed: ${response.status}`);
  }
 
  return response.json();
}

Calling it with a plain search term builds the full URL automatically, including the encoded query string, without the caller ever needing to think about escaping.

javascriptjavascript
searchUsers("taylor", 1, 20);
// Fetches: https://api.example.com/users/search?q=taylor&page=1&limit=20

Never concatenate user input directly into a URL. It escapes special characters like spaces, ampersands, and quotes that would otherwise break the URL.

Fetch with async/await and Promise.all

Fetch works naturally with async/await and all promise combinators, since every fetch call is just an ordinary promise underneath.

javascriptjavascript
// Parallel independent fetches
async function loadPageData(userId) {
  const [user, posts, settings] = await Promise.all([
    fetch(`/api/users/${userId}`).then((r) => r.json()),
    fetch(`/api/posts?userId=${userId}`).then((r) => r.json()),
    fetch(`/api/settings/${userId}`).then((r) => r.json()),
  ]);
 
  return { user, posts, settings };
}

All three requests start at the same time. The function waits for all of them to finish, then returns the combined result. For more on parallel patterns, see JavaScript Async Await Guide: Complete Tutorial.

Common fetch mistakes

Not checking response.ok. This is the most common mistake. A 404 or 500 does not reject, so your code proceeds as if the request succeeded, then crashes when it tries to use the missing data.

javascriptjavascript
// Mistake: no ok check
const data = await fetch(url).then((r) => r.json()); // Could be an error page

Checking ok first, before parsing the body, catches the error status before it can cause a confusing downstream crash further down in the function.

javascriptjavascript
// Correct: check ok first
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();

Forgetting that .json() returns a promise. Calling response.json() without awaiting it and then expecting the parsed object back directly instead of a pending promise.

javascriptjavascript
// Mistake
const user = response.json(); // This is a Promise, not an object
console.log(user.name); // undefined

Adding the missing await fixes it, since the variable then holds the resolved object instead of the pending promise the mistake version left behind.

javascriptjavascript
// Correct
const user = await response.json();
console.log(user.name); // The actual name

Stringifying the body twice. Calling JSON.stringify() twice by accident sends the server a double-encoded string instead of a normal JSON body.

javascriptjavascript
// Mistake: double stringify
body: JSON.stringify(JSON.stringify({ name: "Taylor" }))

Stringifying the object exactly once produces the plain JSON body the server actually expects, without the extra layer of escaped quotes the mistake version sends.

javascriptjavascript
// Correct
body: JSON.stringify({ name: "Taylor" })

Mixing fetch with XMLHttpRequest patterns. Fetch is promise-based. Do not try to use event listeners like onload or onerror, or check a readyState property. Those patterns belong to the older XMLHttpRequest, not fetch.

Rune AI

Rune AI

Key Insights

  • fetch returns a promise that resolves to a Response object, even for HTTP errors.
  • Use response.json() to parse JSON bodies. The method returns a promise.
  • Always check response.ok before parsing. A 404 or 500 does not reject the fetch promise.
  • Pass method, headers, and body in an options object for POST and other non-GET requests.
  • fetch integrates naturally with async/await and works with all promise combinators.
RunePowered by Rune AI

Frequently Asked Questions

Does fetch reject on HTTP errors like 404 or 500?

No. fetch only rejects on network failures. An HTTP 404 or 500 response still resolves the promise. You must check response.ok or response.status manually to detect server errors.

How do I cancel a fetch request?

Pass an AbortController's signal to the fetch options. Call controller.abort() to cancel the request. The fetch promise rejects with an AbortError.

Is fetch available in Node.js?

Yes. fetch is available globally in Node.js 18 and later. For older versions, you need the node-fetch package.

Conclusion

The Fetch API is the standard way to make HTTP requests in modern JavaScript. It returns promises, works naturally with async/await, and handles all common request patterns: GET, POST, JSON parsing, headers, and error checking. The one thing to remember: always check response.ok.