What are the differences between `XMLHttpRequest` and `fetch()` in JavaScript and browsers?
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, whilefetch()
utilizes promise chaining.fetch()
provides more flexibility in headers and request bodies.fetch()
support cleaner error handling withcatch()
.- Handling caching with
XMLHttpRequest
is difficult but caching is supported byfetch()
by default in theoptions.cache
object (cache
value of second parameter) tofetch()
orRequest()
. fetch()
requires anAbortController
for cancelation, while forXMLHttpRequest
, it providesabort()
property.XMLHttpRequest
has good support for progress tracking, whichfetch()
lacks.XMLHttpRequest
is only available in the browser and not natively supported in Node.js environments. On the other handfetch()
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 datafetch('https://jsonplaceholder.typicode.com/todos/1').then((response) => response.json()).then((data) => console.log(data));// Text datafetch('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 XMLHttpRequest
s 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 theProgressEvent
is a 64-bit integer indicating the amount of work already performed (bytes uploaded/downloaded) by the underlying process. - The
total
field on theProgressEvent
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 theContent-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.