Quiz

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

Topics
JavaScriptNetworking
Edit on GitHub

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 event callbacks, while fetch() utilizes promise chaining.
  • fetch() provides more flexibility in headers and request bodies.
  • fetch() support cleaner error handling with catch().
  • Handling caching with XMLHttpRequest is difficult but caching is supported by fetch() by default in the options.cache object (cache value of second parameter) to fetch() or Request().
  • fetch() requires an AbortController for cancelation, while for XMLHttpRequest, it provides abort() property.
  • XMLHttpRequest has good support for progress tracking, which fetch() lacks.
  • XMLHttpRequest is only available in the browser and not natively supported in Node.js environments. On the other hand fetch() is part of the JavaScript language and is supported on all modern JavaScript runtimes.

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 XMLHttpRequest and fetch() support setting request headers. However, fetch() provides more flexibility in terms of setting headers, as it supports custom headers and allows for more complex header configurations.

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 support types likes 'text', 'arraybuffer', 'blob', 'document' and 'json'.

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();

On the other hand, fetch() provides a unified Response object with then method 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

Both support error handling but fetch() provides more flexibility in terms of error handling, as it supports handling errors using the .catch() method.

XMLHttpRequest supports error handling using the onerror event:

xhr.onerror = function () {
console.log('Error occurred');
};

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

fetch('https://jsonplaceholder.typicode.com/todos/1')
.then((response) => response.json())
.then((data) => console.log(data))
.catch((error) => console.log('Error occurred: ' + error));

Caching control

Handling caching with XMLHttpRequest is difficult, and you might need to add a random value to the query string in order to get around the browser cache. Caching is supported by fetch() by default in the second parameter of the options object:

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 it to 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.log('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 request.

On the other hand, the fetch() API does not offer any convenient way to track upload progress. It can be implemented by monitoring the body of the Response object as a fraction of the Content-Length header, but it's quite complicated.

Choosing between XMLHttpRequest and fetch()

In modern development scenarios, fetch() is the preferred choice due to its cleaner syntax, promise-based approach, and improved handling of features like error handling, headers, and CORS.

Further reading

Edit on GitHub