JavaScript Fetch API and AJAX: Make HTTP Requests in the Browser

The Fetch API is the modern browser standard for making HTTP requests from JavaScript. It is the main tool for loading data, sending form values, and talking to web APIs without reloading the page.

Quick answer: Use fetch() to send a request and receive a Promise that resolves to a Response object. Then read the body with methods like response.json() or response.text(), and check response.ok before trusting success.

Difficulty: Beginner

You'll understand this better if you know: basic JavaScript functions, promises, objects, and the idea of sending data between a browser and a server.

1. What Is Fetch API & AJAX?

AJAX means making asynchronous requests from a web page so it can update data without a full page reload. The Fetch API is the modern browser API used to do that in JavaScript.

AJAX is a broader idea, while Fetch API is one modern implementation of that idea in JavaScript.

2. Why Fetch API & AJAX Matters

Most interactive sites need data from servers: search results, product lists, chat messages, user profiles, and form submissions. Fetch lets you update part of a page instead of refreshing the whole document.

That improves user experience, reduces unnecessary page loads, and makes web apps feel faster. It also gives you a cleaner, more readable API than older techniques.

Use Fetch when you want to:

Do not use Fetch for everything. It is for network requests, not for local computation, timers, or DOM-only interactions.

3. Basic Syntax or Core Idea

The simplest Fetch call passes a URL and returns a promise. You then inspect the response and read the body in the format you need.

Minimal GET request

This example requests JSON from an API and logs the parsed data.

fetch("/api/users")
  .then((response) => response.json())
  .then((data) => {
    console.log(data);
  })
  .catch((error) => {
    console.error("Request failed:", error);
  });

In this pattern, fetch() starts the request, response.json() reads the body as JSON, and catch() handles network-level failures.

Using async and await

Most developers prefer async and await because it reads like synchronous code.

async function loadUsers() {
  try {
    const response = await fetch("/api/users");
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error("Request failed:", error);
  }
}

loadUsers();

This version does the same work, but it is often easier to extend with more steps and error handling.

4. Step-by-Step Examples

Example 1: Reading JSON data

This is the most common Fetch use case. You request an endpoint that returns JSON and convert the response body to a JavaScript value.

async function loadProfile() {
  const response = await fetch("/api/profile");
  const profile = await response.json();

  console.log(profile.name);
}

The key point is that Fetch does not automatically parse the body. You choose the method that matches the response format.

Example 2: Sending JSON with POST

When you submit data to a server, you usually set a method, headers, and a JSON body.

async function createUser() {
  const response = await fetch("/api/users", {
    method: "POST",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      name: "Ava",
      role: "editor"
    })
  });

  const result = await response.json();
  console.log(result);
}

This example shows the standard pattern for JSON APIs: stringify the body and set the content type header.

Example 3: Handling non-JSON text

Not every endpoint returns JSON. You can read plain text, HTML fragments, or other text responses.

async function loadStatusText() {
  const response = await fetch("/status.txt");
  const text = await response.text();

  document.querySelector("#status").textContent = text;
}

Use response.text() when the server sends readable text instead of structured JSON.

Example 4: Downloading a file as a Blob

Fetch can also download binary data, such as images or PDFs, by reading the body as a blob.

async function downloadReport() {
  const response = await fetch("/reports/monthly.pdf");
  const file = await response.blob();
  const url = URL.createObjectURL(file);

  const link = document.createElement("a");
  link.href = url;
  link.download = "monthly.pdf";
  link.click();

  URL.revokeObjectURL(url);
}

This pattern is useful when the server sends a file that the browser should save or preview.

5. Practical Use Cases

These are all examples of browser-side data exchange, which is exactly where Fetch shines.

6. Common Mistakes

Mistake 1: Assuming a non-OK response throws an error

Fetch only rejects the promise for network-level failures, not for HTTP status codes like 404 or 500. A response can be returned successfully even when the server says the request failed.

Problem: This code treats every resolved response as success, so it may try to parse an error page as JSON or continue with invalid data.

async function loadUser() {
  const response = await fetch("/api/user/999");
  const user = await response.json();
  console.log(user);
}

Fix: Check response.ok or response.status before reading the body.

async function loadUser() {
  const response = await fetch("/api/user/999");

  if (!response.ok) {
    throw new Error(`Request failed with status ${response.status}`);
  }

  const user = await response.json();
  console.log(user);
}

The corrected version works because it separates transport success from HTTP success.

Mistake 2: Forgetting to stringify JSON request bodies

The request body must be a string, Blob, FormData, or another supported body type. A plain JavaScript object is not sent as JSON automatically.

Problem: This code passes an object directly, which results in an invalid request body for most JSON APIs.

await fetch("/api/users", {
  method: "POST",
  headers: {
    "Content-Type": "application/json"
  },
  body: {
    name: "Mia"
  }
});

Fix: Convert the object with JSON.stringify() before sending it.

await fetch("/api/users", {
  method: "POST",
  headers: {
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    name: "Mia"
  })
});

This version works because the server receives valid JSON text instead of a raw object.

Mistake 3: Trying to read the body more than once

A response body is a stream. Once you consume it with json(), text(), or another body-reading method, it is no longer available.

Problem: This code tries to read the same response twice, which leads to a body-used error or empty results.

async function inspectResponse() {
  const response = await fetch("/api/user");
  const firstRead = await response.text();
  const secondRead = await response.json();

  console.log(firstRead, secondRead);
}

Fix: Read the body once, or use response.clone() if you truly need two copies.

async function inspectResponse() {
  const response = await fetch("/api/user");
  const copy = response.clone();

  const firstRead = await response.text();
  const secondRead = await copy.json();

  console.log(firstRead, secondRead);
}

The fixed version works because each stream is consumed only once.

7. Best Practices

Practice 1: Always handle HTTP errors explicitly

Do not assume success just because the promise resolved. Checking response.ok prevents subtle bugs when the server returns an error page or error JSON.

async function getData() {
  const response = await fetch("/api/data");

  if (!response.ok) {
    throw new Error("Server returned an error.");
  }

  return await response.json();
}

This makes your code fail loudly instead of continuing with invalid data.

Practice 2: Separate request logic from UI logic

Keep your network code in a small function so it is easier to test and reuse. Your UI code can then focus on rendering states like loading, success, and error.

async function fetchProducts() {
  const response = await fetch("/api/products");
  if (!response.ok) {
    throw new Error("Failed to load products");
  }
  return await response.json();
}

By isolating request code, you avoid duplicating the same fetch pattern throughout your app.

Practice 3: Use the right body type for the job

Choose JSON for structured data, FormData for form submissions with files, and plain text when the server expects text. Matching the body type avoids server-side parsing errors.

const formData = new FormData();
formData.append("avatar", fileInput.files[0]);
formData.append("displayName", "Jordan");

await fetch("/api/profile", {
  method: "POST",
  body: formData
});

This is the preferred choice when uploading files because the browser formats the multipart request correctly.

8. Limitations and Edge Cases

These are the cases where developers often say Fetch is “not working,” when the real issue is status handling, browser policy, or response body behavior.

9. Practical Mini Project

In this mini project, a button loads a list of users from an API and shows either the names or an error message. This combines the core Fetch workflow with basic DOM updates.

<button id="load-users">Load Users</button>
<p id="message"></p>
<ul id="user-list"></ul>
const button = document.querySelector("#load-users");
const message = document.querySelector("#message");
const list = document.querySelector("#user-list");

async function loadUsers() {
  message.textContent = "Loading...";
  list.innerHTML = "";

  try {
    const response = await fetch("/api/users");

    if (!response.ok) {
      throw new Error("Could not load users.");
    }

    const users = await response.json();

    for (const user of users) {
      const item = document.createElement("li");
      item.textContent = user.name;
      list.appendChild(item);
    }

    message.textContent = "Users loaded successfully.";
  } catch (error) {
    message.textContent = `Error: ${error.message}`;
  }
}

button.addEventListener("click", loadUsers);

This small app demonstrates the full Fetch flow: trigger a request, wait for the response, handle success, and display errors when something goes wrong.

10. Key Points

11. Practice Exercise

Build a small feature that loads a list of posts from /api/posts when the page loads.

Expected output: The page should first show “Loading...”, then replace it with a numbered list of post titles or an error message if the request fails.

Hint: Use response.ok before calling response.json().

Solution:

const status = document.querySelector("#status");
const postsList = document.querySelector("#posts");

async function loadPosts() {
  status.textContent = "Loading...";
  postsList.innerHTML = "";

  try {
    const response = await fetch("/api/posts");

    if (!response.ok) {
      throw new Error("Failed to load posts.");
    }

    const posts = await response.json();

    for (const post of posts) {
      const item = document.createElement("li");
      item.textContent = post.title;
      postsList.appendChild(item);
    }

    status.textContent = "Posts loaded.";
  } catch (error) {
    status.textContent = `Error: ${error.message}`;
  }
}

loadPosts();

12. Final Summary

The Fetch API is the standard way to make browser-based HTTP requests in modern JavaScript. It supports common request methods, different response formats, and promise-based handling that works well with async and await.

AJAX is the larger idea of asynchronous client-server communication, and Fetch is the API most developers now use to implement it. Once you understand response handling, status checks, and request bodies, you can build fast, interactive pages without reloading.

If you want to go further, next learn about CORS, request headers, and handling forms with FormData.