Fetching Data: fetch vs XMLHttpRequest

· 4 min read · Updated March 7, 2026 · beginner
fetch xmlhttprequest http ajax beginner

Making HTTP requests is fundamental to building modern web applications. Whether you are fetching data from an API, submitting a form, or loading resources from a CDN, you need a way to communicate with servers. For years, XMLHttpRequest was the only option in browsers. Then the Fetch API arrived, offering a cleaner and more powerful alternative.

This tutorial compares both approaches, shows you how each works, and helps you decide which to use in your projects.

What is XMLHttpRequest?

XMLHttpRequest (XHR) is a browser API that has existed since the late 1990s. Despite its name, it works with any data format—not just XML. It was the backbone of early AJAX applications and enabled the dynamic web experiences we have today.

Making a Request with XMLHttpRequest

const xhr = new XMLHttpRequest();
xhr.open("GET", "https://jsonplaceholder.typicode.com/posts/1");
xhr.responseType = "json";

xhr.onload = function() {
  if (xhr.status >= 200 && xhr.status < 300) {
    console.log("Success:", xhr.response);
  } else {
    console.error("Error:", xhr.statusText);
  }
};

xhr.onerror = function() {
  console.error("Network error occurred");
};

xhr.send();

Notice the pattern: you create an XHR object, configure it with open(), attach event listeners for onload and onerror, then send the request with send().

Handling Different HTTP Methods

// POST request with XMLHttpRequest
const xhrPost = new XMLHttpRequest();
xhrPost.open("POST", "https://jsonplaceholder.typicode.com/posts");
xhrPost.setRequestHeader("Content-Type", "application/json");
xhrPost.responseType = "json";

xhrPost.onload = function() {
  console.log("Created:", xhrPost.response);
};

const data = JSON.stringify({
  title: "My Post",
  body: "Content here",
  userId: 1
});

xhrPost.send(data);

What is the Fetch API?

The Fetch API is a modern interface for making HTTP requests. It uses Promises, making it easier to work with asynchronous code compared to XHR’s event-based approach.

Making a Request with Fetch

fetch("https://jsonplaceholder.typicode.com/posts/1")
  .then(response => {
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    return response.json();
  })
  .then(data => {
    console.log("Success:", data);
  })
  .catch(error => {
    console.error("Error:", error);
  });

The response is a Promise that resolves to a Response object. You need to call a method like .json(), .text(), or .blob() to extract the body.

Handling Different HTTP Methods

// POST request with fetch
fetch("https://jsonplaceholder.typicode.com/posts", {
  method: "POST",
  headers: {
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    title: "My Post",
    body: "Content here",
    userId: 1
  })
})
  .then(response => response.json())
  .then(data => console.log("Created:", data))
  .catch(error => console.error("Error:", error));

Using Async/Await with Fetch

The Fetch API works beautifully with async/await syntax, making asynchronous code read like synchronous code:

async function fetchPost(id) {
  try {
    const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
    
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    
    const data = await response.json();
    console.log("Success:", data);
    return data;
  } catch (error) {
    console.error("Error:", error);
  }
}

fetchPost(1);

Key Differences

FeatureXMLHttpRequestFetch API
SyntaxEvent-basedPromise-based
Request configurationopen(), setRequestHeader()Options object
Response handlingresponse property.json(), .text() methods
Progress trackingonprogress eventNot built-in
CORS handlingManualAutomatic
Aborting requestsabort() methodAbortController

When to Use Fetch

Use the Fetch API for most new projects:

  • Cleaner, more readable code with Promises and async/await
  • Better error handling with HTTP status codes
  • Built-in support for Service Workers
  • More flexible request configuration
// Fetch with timeout using AbortController
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);

fetch("https://jsonplaceholder.typicode.com/posts/1", {
  signal: controller.signal
})
  .then(response => response.json())
  .then(data => {
    clearTimeout(timeoutId);
    console.log(data);
  })
  .catch(error => {
    clearTimeout(timeoutId);
    if (error.name === "AbortError") {
      console.log("Request timed out");
    } else {
      console.error("Error:", error);
    }
  });

When to Use XMLHttpRequest

XMLHttpRequest is still useful in specific scenarios:

  • Supporting very old browsers (Internet Explorer 10 and below)
  • Needing fine-grained progress events for file uploads
  • Working with legacy codebases that already use XHR
// Progress tracking with XMLHttpRequest
const xhr = new XMLHttpRequest();
xhr.open("GET", "https://example.com/large-file");

xhr.onprogress = function(event) {
  if (event.lengthComputable) {
    const percentComplete = (event.loaded / event.total) * 100;
    console.log(`Progress: ${percentComplete}%`);
  }
};

xhr.onload = function() {
  console.log("Download complete");
};

xhr.send();

Best Practices

  1. Always check response.ok or status codes — Fetch does not reject on HTTP errors:

    // Wrong: Fetch does not throw on 404
    fetch("/api/user").then(user => console.log(user)); // Will fail
    
    // Correct: Check response.ok first
    fetch("/api/user")
      .then(response => {
        if (!response.ok) throw new Error("Request failed");
        return response.json();
      })
  2. Use AbortController for cancellation — Clean up pending requests:

    const controller = new AbortController();
    fetch(url, { signal: controller.signal });
    // Later: controller.abort();
  3. Set appropriate timeouts — Prevent requests from hanging indefinitely

  4. Handle both network and HTTP errors — Use try/catch with async/await:

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

Summary

The Fetch API is the modern standard for making HTTP requests in browsers. It provides a cleaner Promise-based interface that works naturally with async/await. Use it for all new code.

XMLHttpRequest remains relevant for legacy browser support and specific use cases like progress tracking. However, for most applications, Fetch is the better choice.

In the next tutorial, we’ll explore the Geolocation API for accessing the user’s location data.