---
title: "Fetch vs XHR: A Deep Dive into Web Dev’s Past and Present"
description: "Learn how to create a basic fetch API polyfill using XMLHttpRequest. Understand the evolution from XHR to modern fetch with practical examples."
slug: "fetch-vs-xhr-a-deep-dive-into-web-devs-past-and-present"
url: "https://furkanbaytekin.dev/blogs/fetch-vs-xhr-a-deep-dive-into-web-devs-past-and-present"
author: "Furkan Baytekin"
category: "Software"
tags: []
published: 2025-02-24T07:50:57Z
updated: 2025-02-24T07:50:57Z
readTime: 7
cover: "https://myblogcms.furkanbaytekin.dev/uploads/cover/fetch-vs-xhr-a-deep-dive-into-web-devs-past-and-present+1740326555645"
---

Let’s talk about two staples of HTTP requests in JavaScript: the fetch() API and XMLHttpRequest (XHR). You’re probably using fetch() daily—it’s the modern standard, after all—but XHR? It’s more like that vintage vinyl player in the corner: not spinning much these days, but it’s got a story worth knowing. Understanding fetch versus XHR isn’t about picking a winner (spoiler: fetch already won); it’s about appreciating where we’ve been and why fetch rules the roost in 2025. So, let’s break it down—history, mechanics, and all—with some code to keep it real.

## The Backstory: XHR’s Glory Days and Fetch’s Rise

Picture this: it’s the late ‘90s, and the web is mostly static pages and dial-up screeches. Microsoft drops XMLHttpRequest into IE5 as an ActiveX oddity. Fast forward to the mid-2000s, and XHR becomes the backbone of AJAX—those slick, no-refresh updates powering Gmail and Google Maps. It was a game-changer, letting devs snag data asynchronously with a clunky but effective API. Every browser adopted it, and XHR reigned supreme for years.

Then, in 2015, fetch() arrived, cooked up by the [WHATWG](https://whatwg.org/faq) crew and rolled out in modern browsers like Chrome and Firefox. It wasn’t just a new toy—it was a rethink. Promise-based, streamlined, and free of XHR’s baggage, fetch() was built for a JavaScript world that had outgrown callbacks. By 2025, XHR’s a relic; fetch() is baked into Node.js (since v18) and powers everything from SPAs to APIs. But how’d we get from there to here? Let’s compare.

## How They Work: Fetch vs XHR Under the Hood

Both fetch() and XHR fetch data from servers, but their approaches? Night and day. Here’s the breakdown with examples to show the shift.

### 1. Syntax: Old School vs New School

XHR is verbose and event-driven—think manual gears. Fetch is Promise-based and sleek. Here’s a simple GET request:

**XHR: The Classic Way**

```javascript
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/users', true);
xhr.onload = function () {
  // Yes, this was a best practice back in the day.
  if (xhr.status >= 200 && xhr.status < 300) {
    const data = JSON.parse(xhr.responseText);
    console.log('Users:', data);
  } else {
    console.error('Error:', xhr.status);
  }
};
xhr.onerror = function () {
  console.error('Network issue!');
};
xhr.send();
```

**Fetch: The Modern Way**

```javascript
// Defaults to GET method
fetch('https://api.example.com/users')
  .then(response => {
    if (!response.ok) throw new Error(`Status: ${response.status}`);
    return response.json();
  })
  .then(data => console.log('Users:', data))
  .catch(error => console.error('Oops:', error));
```

Or, with async/await:

```javascript
async function getUsers() {
  try {
    const response = await fetch('https://api.example.com/users');
    if (!response.ok) throw new Error('Something’s off!');
    const data = await response.json();
    console.log('Users:', data);
  } catch (error) {
    console.error('Oops:', error);
  }
}
getUsers();
```

XHR’s a slog—setting up events, parsing JSON manually. Fetch hands you a clean chain and built-in methods like `response.json()`. It’s no wonder XHR’s faded out.

### 2. Error Handling

XHR flags errors via status codes in `onload`—you sort it out. Fetch only rejects Promises on network failures; HTTP errors like 404 or 500 resolve as “successful” unless you check `response.ok`.

**XHR: Status Check**

```javascript
xhr.onload = function () {
  if (xhr.status === 404) {
    console.error('Not found!');
  } else if (xhr.status === 200) {
    console.log(JSON.parse(xhr.responseText));
  }
};
```

**Fetch: Watch That `ok`**
```javascript
fetch('https://api.example.com/missing')
  .then(response => {
    if (!response.ok) throw new Error('Not found!');
    return response.json();
  })
  .catch(error => console.error(error));
```

### 3. Features: What They Bring to the Table

XHR had some neat tricks—like `onprogress` for tracking uploads:

```javascript
xhr.upload.onprogress = event => {
  const percent = Math.round((event.loaded / event.total) * 100);
  console.log(`Uploaded ${percent}%`);
};
```

Fetch doesn’t do this natively (streams can approximate it), but it counters with modern wins: readable streams for big data and `AbortController` for canceling requests:

**Aborting a Request**

```javascript
const controller = new AbortController();
fetch('https://api.example.com/big-data', { signal: controller.signal })
  .then(response => response.json())
  .catch(err => console.error(err));
setTimeout(() => controller.abort(), 5000); // Bail after 5s
```

**Progress bar with fetch**

```javascript
async function downloadWithProgress(url) {
  const response = await fetch(url);
  const contentLength = response.headers.get('content-length');
  const total = parseInt(contentLength) || 0;
  let loaded = 0;
  const reader = response.body.getReader();
  const chunks = [];

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    chunks.push(value);
    loaded += value.length;
    const progress = total ? (loaded / total) * 100 : 'unknown';
    console.log(`Downloaded ${progress.toFixed(2)}%`);
    const progressBar = document.querySelector('#progress');
    if (progressBar) {
      progressBar.style.width = `${progress}%`;
      progressBar.textContent = `${progress.toFixed(1)}%`;
    }
  }
  const blob = new Blob(chunks);
  return blob;
}

downloadWithProgress('https://api.example.com/large-file')
  .then(blob => console.log('Download complete:', blob))
  .catch(error => console.error('Download failed:', error));
```

### 4. POST Requests: Sending Stuff

Both can POST, but fetch simplifies it:

**XHR POST**

```javascript
xhr.open('POST', 'https://api.example.com/users', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify({ name: 'Furkan' }));
```

**Fetch POST**

```javascript
fetch('https://api.example.com/users', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'Furkan' })
}) .then(response =>
  response.json()
);
```

Fetch’s options object is just more ergonomic.

## Browser Support: Where They Stand in 2025

Today, fetch() is everywhere: Chrome, Firefox, Safari, Edge, Node.js. It’s the default, no questions asked. XHR? It’s universal too, even in ancient IE6, nobody’s running IE11 unless they’re stuck in a corporate time capsule. Fetch’s dominance is complete, though older codebases might still lean on XHR for historical reasons.

## Why Fetch Took Over

Fetch didn’t just edge out XHR—it lapped it. Promises and async/await fit today’s JavaScript like a glove, and fetch’s streamlined API cuts the cruft. It’s built for streams, service workers, and a web that’s all about real-time and scale. XHR’s still got niche uses (progress tracking, anyone?), but it’s a museum piece in 2025—respected, not used.

## A Fetch Polyfill: Bridging the Past

For fun, here’s a simple fetch polyfill using XHR—think of it as a history lesson in code. It’s not for daily use (fetch is native now), but it shows how to mimic fetch in a pinch:

```javascript
if (!window.fetch) {
  window.fetch = function (url, options) {
    if (!options) {
      options = {};
    }

    return new Promise((resolve, reject) => {
      const xhr = new XMLHttpRequest();
      const method = (options.method || 'GET').toUpperCase();

      xhr.open(method, url, true);

      if (options.headers) {
        for (const [key, value] of Object.entries(options.headers)) {
          xhr.setRequestHeader(key, value);
        }
      }

      xhr.responseType = options.responseType || 'text';

      xhr.onload = function () {
        const response = {
          ok: xhr.status >= 200 && xhr.status < 300,
          status: xhr.status,
          statusText: xhr.statusText,
          json: () => Promise.resolve(
            xhr.responseType === 'json' ? xhr.response : JSON.parse(xhr.responseText || xhr.response)
          ),
          text: () => Promise.resolve(
            xhr.responseType === 'text' ? xhr.response : String(xhr.response)
          ),
          blob: () => Promise.resolve(
            xhr.responseType === 'blob' ? xhr.response : new Blob([xhr.responseText || xhr.response])
          )
        };

        if (response.ok) {
          resolve(response);
        } else {
          reject(new Error(`Status: ${xhr.status}`));
        }
      };

      xhr.onerror = () => reject(new Error('Network error'));
      xhr.send(method === 'GET' || method === 'HEAD' ? null : options.body);
    });
  };
}

// Test it
fetch('https://api.example.com/users')
  .then(res => res.json())
  .then(data => console.log(data))
  .catch(err => console.error(err));
```

**Step-by-Step:**
1. **Check Support**: If `fetch` isn’t there, define it.
2. **Options**: If no options are provided, create an empty object.
3. **Promise It Up**: Wrap XHR in a Promise to match fetch’s style.
4. **Map Options**: Translate fetch’s `method`, `headers`, and `body` to XHR.
5. **Fake a Response**: Build a fetch-like object with `ok`, `json()`, etc.
6. **Handle Errors**: Reject on network issues or bad statuses.

It’s barebones—no streams or aborting—but it mimics fetch’s core. Real-world polyfills like `whatwg-fetch` do more, but this shows the bridge from XHR to fetch.

## Final Thoughts

Fetch vs XHR isn’t a debate anymore—fetch is the present and future. Still, knowing XHR’s quirks and legacy gives you context for why fetch feels so good. It’s like appreciating black-and-white TV before streaming 4K. Next time you’re chaining a `.then()` or awaiting a response, give a nod to XHR—it got us here.

---

Album of the day:

<iframe style="border-radius:12px" src="https://open.spotify.com/embed/album/58MQ0PLijVHePUonQlK76Y?utm_source=generator" width="100%" height="152" frameBorder="0" allowfullscreen="" allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture" loading="lazy"></iframe>
