Quiz

What are the differences between `XMLHttpRequest` and `fetch()` in JavaScript and browsers?

Topics
JavaScriptNetworking

TL;DR

XMLHttpRequest (XHR) and fetch() API are both used for asynchronous HTTP requests in JavaScript (AJAX). fetch() offers a cleaner syntax, promise-based approach, and more modern feature set compared to XHR. However, there are some differences:

  • XMLHttpRequest uses event callbacks, while fetch() utilizes promise chaining.
  • Both APIs support request headers and common body types; Fetch exposes Headers, Request, and Response abstractions and integrates with streams.
  • A Fetch promise rejects for network, CORS, and abort failures but fulfills for HTTP error statuses, so callers must check response.ok or response.status.
  • Both APIs use the browser's HTTP cache. Fetch additionally exposes a cache request option.
  • fetch() requires an AbortController for cancelation, while XMLHttpRequest provides an abort() method.
  • XHR exposes convenient upload and download progress events. Fetch response bodies are streams, so download progress can be measured manually, but browsers still lack an equally convenient standard Fetch upload-progress API.
  • XHR is a browser API. Fetch is a web standard also implemented by current Node.js and several other runtimes; check the target runtime rather than assuming universal support.

These days fetch() is preferred for its cleaner syntax and modern features.


XMLHttpRequest vs fetch()

Both XMLHttpRequest (XHR) and fetch() are ways to make asynchronous HTTP requests in JavaScript. However, they differ significantly in syntax, promise handling, and feature set.

Syntax and usage

XMLHttpRequest is event-driven and requires attaching event listeners to handle response/error states. The basic syntax for creating an XMLHttpRequest object and sending a request is as follows:

const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://jsonplaceholder.typicode.com/todos/1', true);
xhr.responseType = 'json';
xhr.onload = function () {
if (xhr.status === 200) {
console.log(xhr.response);
}
};
xhr.send();

xhr is an instance of the XMLHttpRequest class. The open method is used to specify the request method, URL, and whether the request should be asynchronous. The onload event is used to handle the response, and the send method is used to send the request.

fetch() provides a more straightforward and intuitive way of making HTTP requests. It is Promise-based and returns a promise that resolves with the response or rejects with an error. The basic syntax for making a GET request using fetch() is as follows:

fetch('https://jsonplaceholder.typicode.com/todos/1')
.then((response) => response.text())
.then((data) => console.log(data));

Request headers

Both APIs can set permitted request headers. Fetch also provides a dedicated Headers abstraction; browsers restrict certain headers for security regardless of which API is used.

XMLHttpRequest supports setting request headers using the setRequestHeader method:

xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('Authorization', 'Bearer YOUR_TOKEN');

For fetch(), headers are passed as an object in the second argument to fetch():

fetch('https://jsonplaceholder.typicode.com/todos/1', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer YOUR_TOKEN',
},
body: JSON.stringify({
name: 'John Doe',
age: 30,
}),
});

Request body

Both XMLHttpRequest and fetch() support sending request bodies. However, fetch() provides more flexibility in terms of sending request bodies, as it supports sending JSON data, form data, and more.

XMLHttpRequest supports sending request bodies using the send method:

const xhr = new XMLHttpRequest();
xhr.open('POST', 'https://jsonplaceholder.typicode.com/todos/1', true);
xhr.send(
JSON.stringify({
name: 'John Doe',
age: 30,
}),
);

fetch() supports sending request bodies using the body property in the second argument to fetch():

fetch('https://jsonplaceholder.typicode.com/todos/1', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'John Doe',
age: 30,
}),
});

Response handling

XMLHttpRequest provides a responseType property to set the response format that we are expecting. responseType is 'text' by default but it supports types like 'text', 'arraybuffer', 'blob', 'document' and 'json'.

const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://jsonplaceholder.typicode.com/todos/1', true);
xhr.responseType = 'json'; // or 'text', 'blob', 'arraybuffer'
xhr.onload = function () {
if (xhr.status === 200) {
console.log(xhr.response);
}
};
xhr.send();

On the other hand, fetch() provides a unified Response object with methods like .json() and .text() for accessing data.

// JSON data
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then((response) => response.json())
.then((data) => console.log(data));
// Text data
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then((response) => response.text())
.then((data) => console.log(data));

Error handling

The APIs expose errors differently. XHR uses events and status fields. Fetch uses promises, but a fulfilled Fetch promise can still contain an HTTP error response.

XMLHttpRequest supports error handling using the onerror event:

const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://jsonplaceholder.typicod.com/todos/1', true); // Typo in URL
xhr.responseType = 'json';
xhr.onload = function () {
if (xhr.status === 200) {
console.log(xhr.response);
}
};
xhr.onerror = function () {
console.error('Error occurred');
};
xhr.send();

fetch() supports error handling using the catch() method on the returned Promise:

fetch('https://jsonplaceholder.typicod.com/todos/1') // Typo in URL
.then((response) => {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
})
.then((data) => console.log(data))
.catch((error) => console.error('Error occurred: ' + error));

Caching control

Both APIs participate in normal HTTP caching. With Fetch, the cache request option gives code an explicit way to choose how the request interacts with the browser cache:

const res = await fetch('https://jsonplaceholder.typicode.com/todos/1', {
method: 'GET',
cache: 'default',
});

Other values for the cache option include default, no-store, reload, no-cache, force-cache, and only-if-cached.

Cancelation

In-flight XMLHttpRequests can be canceled by running the XMLHttpRequest's abort() method. An abort handler can be attached by assigning to the .onabort property if necessary:

const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://jsonplaceholder.typicode.com/todos/1');
xhr.send();
// ...
xhr.onabort = () => console.log('aborted');
xhr.abort();

Aborting a fetch() requires creating an AbortController object and passing its signal as the signal property of the options object when calling fetch().

const controller = new AbortController();
const signal = controller.signal;
fetch('https://jsonplaceholder.typicode.com/todos/1', { signal })
.then((response) => response.json())
.then((data) => console.log(data))
.catch((error) => console.error('Error occurred: ' + error));
// Abort request.
controller.abort();

Progress support

XMLHttpRequest supports tracking the progress of requests by attaching a handler to the XMLHttpRequest object's progress event. This is especially useful when uploading large files such as videos to track the progress of the upload.

const xhr = new XMLHttpRequest();
// The callback is passed a `ProgressEvent`.
xhr.upload.onprogress = (event) => {
console.log(Math.round((event.loaded / event.total) * 100) + '%');
};

The callback assigned to onprogress is passed a ProgressEvent:

  • The loaded field on the ProgressEvent is a 64-bit integer indicating the amount of work already performed (bytes uploaded/downloaded) by the underlying process.
  • The total field on the ProgressEvent is a 64-bit integer representing the total amount of work that the underlying process is in the progress of performing. When downloading resources, this is the Content-Length value of the HTTP response.

Fetch response bodies are readable streams, so download progress can be measured by reading chunks and comparing their total size with Content-Length when that header is present. Browser Fetch does not yet provide XHR's straightforward upload.onprogress equivalent for upload progress.

Choosing between XMLHttpRequest and fetch()

For new promise-based code, Fetch is usually the default. XHR remains useful when straightforward browser upload-progress events are a requirement. Neither API bypasses CORS, and Fetch's HTTP-status handling must be implemented explicitly.

Further reading

Exercises

Check your understanding
Beta
Check your understanding Exercise
Check your understanding Exercise

Which statements correctly compare XMLHttpRequest and fetch()? Select all that apply.