# How the Fetch API Works Compared to XMLHttpRequest for HTTP Requests

> Understand Fetch API vs XMLHttpRequest for HTTP requests. Discover how Fetch offers cleaner async/await syntax and better control than older callback methods.

- Repository: [Leonardo Maldonado/33-js-concepts](https://github.com/leonardomso/33-js-concepts)
- Tags: deep-dive
- Published: 2026-03-03

---

**The Fetch API provides a promise-based, streaming-capable interface for HTTP requests that replaces the callback-driven XMLHttpRequest, offering cleaner async/await syntax and more granular control over request cancellation and response processing.**

The evolution from **XMLHttpRequest** to the **Fetch API** represents a fundamental shift in how JavaScript handles HTTP communications. According to the `leonardomso/33-js-concepts` repository—a comprehensive collection of essential JavaScript concepts—the Fetch API vs XMLHttpRequest comparison highlights modern asynchronous patterns that have become standard in web development. This guide examines the architectural differences, practical implementations, and advanced capabilities that distinguish these two APIs based on the source code analysis in `docs/concepts/http-fetch.mdx`.

## Core Architectural Differences Between Fetch API and XMLHttpRequest

### Promise-Based vs Callback-Driven Models

The **Fetch API** returns a **Promise** that resolves to a **Response** object as soon as the response headers are available, enabling natural integration with `async/await` syntax. In contrast, **XMLHttpRequest** relies on **event callbacks** (`onload`, `onerror`, `onreadystatechange`) attached to a mutable request object.

As documented in `docs/concepts/http-fetch.mdx`, the Fetch API resolves its promise when headers arrive, allowing the body to be processed as a stream, whereas XHR buffers the entire response before triggering the `load` event.

### Request and Response Object Design

Fetch separates concerns into immutable **Request** and **Response** objects, encouraging a functional programming style. XMLHttpRequest combines request configuration and response handling on the same mutable object, requiring sequential method calls (`open()`, `setRequestHeader()`, `send()`) to configure the request.

## Practical Implementation Comparison

### Basic GET Requests

**Fetch API implementation** using async/await:

```javascript
async function loadUser(id) {
  const response = await fetch(`https://api.example.com/users/${id}`);

  // Detect HTTP errors
  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }

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

```

**XMLHttpRequest implementation** using callbacks:

```javascript
function loadUserXHR(id) {
  const xhr = new XMLHttpRequest();
  xhr.open('GET', `https://api.example.com/users/${id}`);
  xhr.onload = function () {
    if (xhr.status >= 200 && xhr.status < 300) {
      const user = JSON.parse(xhr.responseText);
      console.log(user);
    } else {
      console.error('HTTP error', xhr.status);
    }
  };
  xhr.onerror = function () {
    console.error('Network error');
  };
  xhr.send();
}

```

### POST Requests with JSON

**Fetch API POST request**:

```javascript
async function createUser(data) {
  const response = await fetch('https://api.example.com/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data),
  });

  if (!response.ok) throw new Error(`Failed: ${response.status}`);
  return await response.json();
}

```

**XMLHttpRequest POST request**:

```javascript
function createUserXHR(data) {
  const xhr = new XMLHttpRequest();
  xhr.open('POST', 'https://api.example.com/users');
  xhr.setRequestHeader('Content-Type', 'application/json');

  xhr.onload = function () {
    if (xhr.status >= 200 && xhr.status < 300) {
      console.log(JSON.parse(xhr.responseText));
    } else {
      console.error('HTTP error', xhr.status);
    }
  };
  xhr.onerror = () => console.error('Network error');
  xhr.send(JSON.stringify(data));
}

```

### Request Cancellation

**Fetch API cancellation** using AbortController:

```javascript
const controller = new AbortController();
fetch('/slow-endpoint', { signal: controller.signal })
  .then(r => r.json())
  .catch(err => {
    if (err.name === 'AbortError') console.log('Request canceled');
    else console.error(err);
  });

// Cancel after 2 seconds
setTimeout(() => controller.abort(), 2000);

```

**XMLHttpRequest cancellation**:

```javascript
const xhr = new XMLHttpRequest();
xhr.open('GET', '/slow-endpoint');
xhr.onload = () => console.log(xhr.responseText);
xhr.onerror = () => console.error('Network error');
xhr.send();

// Abort after 2 seconds
setTimeout(() => xhr.abort(), 2000);

```

While both APIs support cancellation, the **AbortController** pattern integrates seamlessly with modern promise chains and async/await syntax, whereas XHR's `abort()` method requires manual state management within callback structures.

## Advanced Capabilities and Streaming

### ReadableStream Support in Fetch API

The Fetch API treats response bodies as **ReadableStream** objects, enabling progressive consumption of data without buffering the entire payload. This is particularly valuable for processing large files or implementing progress indicators. XMLHttpRequest buffers the complete response before triggering the `load` event, consuming more memory for large transfers.

As noted in `docs/concepts/http-fetch.mdx`, the streaming capability represents a fundamental architectural advantage of Fetch over XHR, allowing developers to handle data chunks as they arrive rather than waiting for complete transmission.

### AbortController Integration

Modern request cancellation relies on the **AbortController** interface, which provides a standardized way to abort DOM requests including Fetch. The controller's `signal` property connects to the fetch request, and calling `abort()` triggers a promise rejection with an `AbortError`. This pattern, validated in [`tests/web-platform/http-fetch/http-fetch.test.js`](https://github.com/leonardomso/33-js-concepts/blob/main/tests/web-platform/http-fetch/http-fetch.test.js), provides more ergonomic cancellation than XHR's imperative `abort()` method, especially when managing multiple concurrent requests or integrating with React hooks and other modern frameworks.

## Error Handling Patterns

The Fetch API and XMLHttpRequest handle errors differently, which often causes confusion when migrating legacy code.

**Fetch API error handling** requires explicit checks for HTTP error status codes because the promise only rejects on network failures (DNS errors, connection refused, CORS violations). As documented in `docs/concepts/http-fetch.mdx` under "The #1 Fetch Mistake," developers must verify `response.ok` or `response.status` before processing the body.

**XMLHttpRequest error handling** conflates network and HTTP errors within the same callback structure. The `onerror` callback fires for network failures, while HTTP errors (404, 500) must be detected by checking `xhr.status` inside the `onload` handler.

The test suite in [`tests/web-platform/http-fetch/http-fetch.test.js`](https://github.com/leonardomso/33-js-concepts/blob/main/tests/web-platform/http-fetch/http-fetch.test.js) validates these behaviors, ensuring that Fetch properly distinguishes between network-level rejections and HTTP-level non-ok responses.

## Summary

- The **Fetch API** provides a **promise-based** interface that integrates naturally with `async/await`, while **XMLHttpRequest** relies on **event callbacks** and mutable state.
- **Fetch** separates request and response into immutable objects and supports **streaming** via `ReadableStream`, whereas **XHR** buffers the entire response before processing.
- **Error handling** differs significantly: Fetch promises reject only on network failures, requiring explicit `response.ok` checks for HTTP errors, while XHR uses `onerror` for network issues and status checks within `onload` for HTTP errors.
- **Cancellation** is implemented via **AbortController** in Fetch (promise-friendly) and `xhr.abort()` in XHR (callback-based).
- According to the `leonardomso/33-js-concepts` repository, modern JavaScript development favors Fetch for new code, while XHR remains relevant for legacy browser support and synchronous requests (though synchronous XHR is discouraged).

## Frequently Asked Questions

### Is Fetch API better than XMLHttpRequest for all use cases?

While the **Fetch API** is generally preferred for modern applications due to its promise-based architecture and streaming capabilities, **XMLHttpRequest** still serves specific scenarios. XHR supports **synchronous** requests (though deprecated and discouraged), offers **progress tracking** via `onprogress` events for uploads and downloads, and provides wider compatibility with very old browsers. For most contemporary applications using `async/await` and requiring clean cancellation via **AbortController**, Fetch is the superior choice.

### Why does Fetch not reject on HTTP error status codes like 404 or 500?

The **Fetch API** treats HTTP responses as successful network operations regardless of status code, resolving the promise as long as the server responds. This design follows the **Fetch Living Standard**, which distinguishes between **network errors** (connection failures, DNS issues, CORS violations) and **application-level HTTP responses**. Developers must explicitly check `response.ok` (which verifies status 200-299) or `response.status` to handle HTTP errors. This pattern, documented in `docs/concepts/http-fetch.mdx` as "The #1 Fetch Mistake," requires adjustment when migrating from XHR where error handling is more conflated.

### Can I use Fetch API in Node.js environments?

Yes, but with version considerations. **Node.js** added native **Fetch API** support in version **18** as an experimental global and stabilized it in later versions. Prior to Node 18, developers relied on polyfills like `node-fetch` or `cross-fetch` to use Fetch in server-side JavaScript. The [`package.json`](https://github.com/leonardomso/33-js-concepts/blob/main/package.json) in the `leonardomso/33-js-concepts` repository indicates a modern testing environment using Vitest, which supports the native Fetch implementation. For universal JavaScript code that runs in both browsers and Node.js, ensure you're targeting Node 18+ or include appropriate polyfills.

### How do I track upload or download progress with Fetch API?

Unlike **XMLHttpRequest**, which provides `onprogress` events for granular progress tracking, the **Fetch API** handles progress monitoring through **ReadableStream** consumption. To track download progress, you access `response.body` (a `ReadableStream`) and manually read chunks while calculating progress against the `Content-Length` header if available. For upload progress with Fetch, you must create a `ReadableStream` for the request body and track bytes as they are consumed. This approach is more verbose than XHR's event-based progress tracking but offers greater flexibility for transforming streams during transfer. As noted in `docs/concepts/http-fetch.mdx`, XHR remains preferable when simple progress bars are required without additional stream complexity.