XMLHttpRequest vs the Fetch API: What’s Best for Ajax in 2019?

Craig Buckler
Share

Weighing up whether to use XMLHttpRequest vs Fetch and its modern take on Ajax? We compare the pros and cons of both options.

March 2019 celebrates the 20th anniversary of Ajax. Sort of. The first implementation of XMLHttpRequest shipped in 1999 as an IE5.0 ActiveX component (don’t ask). Before then, there had been ways to pull data from a server without a full-page refresh, but they often relied on clunky techniques such as <script> injection or third-party plugins. Microsoft developed XMLHttpRequest primary for a browser-based alternative to their Outlook email client.

XMLHttpRequest was not a web standard until 2006, but it was implemented in most browsers. Its adoption in Gmail (2004) and Google Maps (2005) led to Jesse James Garrett’s 2005 article AJAX: A New Approach to Web Applications. The new term crystallised developer focus.

AJAX to Ajax

AJAX is a mnemonic for Asynchronous JavaScript and XML. “Asynchronous” definitely, but:

  1. JavaScript was likely, although VBScript and Flash were options
  2. The payload did not need to be XML, although that was popular at the time. Any data format could be used and, today, JSON is normally preferred.

We now use “Ajax” as a generic term for any client-side process which fetches data from a server and updates the DOM dynamically without a full-page refresh. Ajax is a core technique for most web applications and Single-Page Apps (SPAs).

Extreme XMLHttpRequest

The following JavaScript code shows a basic HTTP GET request for http://domain/service using XMLHttpRequest (commonly shortened to XHR):

let xhr = new XMLHttpRequest();
xhr.open('GET', 'http://domain/service');

// request state change event
xhr.onreadystatechange = function() {

  // request completed?
  if (xhr.readyState !== 4) return;

  if (xhr.status === 200) {
    // request successful - show response
    console.log(xhr.responseText);
  }
  else {
    // request error
    console.log('HTTP error', xhr.status, xhr.statusText);
  }
};

// start request
xhr.send();

The XMLHttpRequest object has many other options, events, and response properties. For example, a timeout in milliseconds can be set and detected:

// set timeout
xhr.timeout = 3000; // 3 seconds
xhr.ontimeout = () => console.log('timeout', xhr.responseURL);

and a progress event can report on long-running file uploads:

// upload progress
xhr.upload.onprogress = p => {
  console.log( Math.round((p.loaded / p.total) * 100) + '%') ;
}

The number of options can be bewildering and early implementations of XMLHttpRequest had a few cross-browser inconsistencies. For this reason, most libraries and frameworks offer Ajax wrapper functions to handle the complexity, e.g. the jQuery.ajax() method:

// jQuery Ajax
$.ajax('http://domain/service')
  .done(data => console.log(data))
  .fail((xhr, status) => console.log('error:', status));

Fast Forward to Fetch

The Fetch API is a modern alternative to XMLHttpRequest. The generic Headers, Request, and Response interfaces provide consistency while Promises permit easier chaining and async/await without callbacks. The XHR example above can be converted to far simpler Fetch-based code which even parses the returned JSON:

fetch(
    'http://domain/service',
    { method: 'GET' }
  )
  .then( response => response.json() )
  .then( json => console.log(json) )
  .catch( error => console.error('error:', error) );

Fetch is clean, elegant, simpler to understand, and heavily used in PWA Service Workers. Why wouldn’t you use it instead of the ancient XMLHttpRequest?

Unfortunately, web development is never that clear-cut. Fetch is not a full drop-in replacement for Ajax techniques yet…

Browser Support

The Fetch API is reasonably well-supported, but it will fail in all editions of Internet Explorer. People using releases of Chrome, Firefox and Safari older than 2017 may also experience problems. Those users may form a tiny proportion of your user base … or it could be a major client. Always check before you start coding!

In addition, the Fetch API is newer and receives more ongoing changes than the mature XHR object. Those updates are unlikely to break code, but expect some maintenance work over the coming years.

Cookieless by Default

Unlike XMLHttpRequest, not all implementations of Fetch will send cookies so your application’s authentication could fail. The problem can be fixed by changing the initiation options passed in the second argument, e.g.

fetch(
    'http://domain/service',
    {
      method: 'GET',
      credentials: 'same-origin'
    }
  )

Errors Are Not Rejected

Surprisingly, an HTTP error such as a 404 Page Not Found or 500 Internal Server Error does not cause the Fetch Promise to reject; the .catch() is never run. It will normally resolve with the response.ok status set to false.

Rejection only occurs if a request cannot be completed, e.g. a network failure. This can make error trapping more complicated to implement.

Timeouts Are Not Supported

Fetch does not support timeouts and the request will continue for as long as the browser chooses. Further code is required to either wrap the Fetch in another Promise, e.g.

// fetch with a timeout
function fetchTimeout(url, init, timeout = 3000) {
  return new Promise((resolve, reject) => {
    fetch(url, init)
      .then(resolve)
      .catch(reject);
    setTimeout(reject, timeout);
  }
}

… or perhaps use Promise.race() to which resolves when either a fetch or a timeout completes first, e.g.

Promise.race([
  fetch('http://url', { method: 'GET' }),
  new Promise(resolve => setTimeout(resolve, 3000))
])
  .then(response => console.log(response))

Aborting a Fetch

It is easy to end an XHR request with xhr.abort() and, if necessary, detect such an event with an xhr.onabort function.

Aborting a Fetch was not possible for several years but it is now supported in browsers which implement the AbortController API. This triggers a signal which can be passed to the Fetch initiation object:

const controller = new AbortController();

fetch(
  'http://domain/service',
  {
    method: 'GET'
    signal: controller.signal
  })
  .then( response => response.json() )
  .then( json => console.log(json) )
  .catch( error => console.error('Error:', error) );

Fetch can be aborted by calling controller.abort();. The Promise rejects so the .catch() function is called.

No Progress

At the time of writing, Fetch has no support for progress events. It is therefore impossible to report the status of file uploads or similar large form submissions.

XMLHttpRequest vs the Fetch API?

Ultimately, the choice is yours … unless your application has IE clients who demand upload progress bars.

For simpler Ajax calls, XMLHttpRequest is lower-level, more complicated, and you will require wrapper functions. Unfortunately, so will Fetch once you start to consider the complexities of timeouts, call aborts, and error trapping.

You could opt for a Fetch polyfill in conjunction with a Promise polyfill so it’s possible to write Fetch code in IE. However, XHR is used as the fallback; not every option will work as expected, e.g. cookies will be sent regardless of settings.

Fetch is the future. However, the API is relatively new, it does not provide all XHR functionality, and some options are cumbersome. Use it with caution for the next few years.