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:
XMLHttpRequestuses event callbacks, whilefetch()utilizes promise chaining.- Both APIs support request headers and common body types; Fetch exposes
Headers,Request, andResponseabstractions 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.okorresponse.status. - Both APIs use the browser's HTTP cache. Fetch additionally exposes a
cacherequest option. fetch()requires anAbortControllerfor cancelation, whileXMLHttpRequestprovides anabort()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 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
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 URLxhr.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
loadedfield on theProgressEventis a 64-bit integer indicating the amount of work already performed (bytes uploaded/downloaded) by the underlying process. - The
totalfield on theProgressEventis 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-Lengthvalue 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
- Fetch API | MDN
- XMLHttpRequest | MDN
- Request: cache property | MDN
- XMLHttpRequest | JavaScript.info
- This API is so Fetching! - Mozilla Hacks