XMLHttpRequest is a built-in browser object that allows to make HTTP requests in JavaScript.

Despite of having the word “XML” in its name, it can operate on any data, not only in XML format.

Asynchronous XMLHttpRequest

XMLHttpRequest has two modes of operation: synchronous and asynchronous.

First let’s see the asynchronous variant as it’s used in the majority of cases.

The code below loads the URL at /article/xmlhttprequest/hello.txt from the server and shows its content on-screen:

// 1. Create a new XMLHttpRequest object
let xhr = new XMLHttpRequest();

// 2. Configure it: GET-request for the URL /article/.../hello.txt
xhr.open('GET', '/article/xmlhttprequest/hello.txt');

// 3. Send the request over the network
xhr.send();

// 4. This will be called after the response is received
xhr.onload = function() {
  if (xhr.status != 200) { // analyze HTTP status of the response
    // if it's not 200, consider it an error
    alert(xhr.status + ': ' + xhr.statusText); // e.g. 404: Not Found
  } else {
    // show the result
    alert(xhr.responseText); // responseText is the server response
  }
};

As we can see, there are several methods of XMLHttpRequest here. Let’s cover them.

Setup: “open”

The syntax:

xhr.open(method, URL, async, user, password)

This method is usually called first after new XMLHttpRequest. It specifies the main parameters of the request:

  • method – HTTP-method. Usually "GET" or "POST", but we can also use TRACE/DELETE/PUT and so on.
  • URL – the URL to request. Can use any path and protocol, but there are cross-domain limitations called “Same Origin Policy”. We can make any requests to the same protocol://domain:port that the current page comes from, but other locations are “forbidden” by default (unless they implement special HTTP-headers, we’ll cover them in chapter [todo]).
  • async – if the third parameter is explicitly set to false, then the request is synchronous, otherwise it’s asynchronous. We’ll talk more about that in this chapter soon.
  • user, password – login and password for basic HTTP auth (if required).

Please note that open call, contrary to its name, does not open the connection. It only configures the request, but the network activity only starts with the call of send.

Send it out: “send”

The syntax:

xhr.send([body])

This method opens the connection and sends the request to server. The optional body parameter contains the request body. Some request methods like GET do not have a body. And some of them like POST use body to send the data. We’ll see examples with a body in the next chapter.

Cancel: abort and timeout

If we changed our mind, we can terminate the request at any time. The call to xhr.abort() does that:

xhr.abort(); // terminate the request

We can also specify a timeout using the corresponding property:

xhr.timeout = 10000;

The timeout is expressed in ms. If the request does not succeed within the given time, it gets canceled automatically.

Events: onload, onerror etc

A request is asynchronous by default. In other words, the browser sends it out and allows other JavaScript code to execute.

After the request is sent, xhr starts to generate events. We can use addEventListener or on<event> properties to handle them, just like with DOM objects.

The modern specification lists following events:

  • loadstart – the request has started.
  • progress – the browser received a data packet (can happen multiple times).
  • abort – the request was aborted by xhr.abort().
  • error – an network error has occured, the request failed.
  • load – the request is successful, no errors.
  • timeout – the request was canceled due to timeout (if the timeout is set).
  • loadend – the request is done (with an error or without it)
  • readystatechange – the request state is changed (will cover later).

Using these events we can track successful loading (onload), errors (onerror) and the amount of the data loaded (onprogress).

Please note that errors here are “communication errors”. In other words, if the connection is lost or the remote server does not respond at all – then it’s the error in the terms of XMLHttpRequest. Bad HTTP status like 500 or 404 are not considered errors.

Here’s a more feature-full example, with errors and a timeout:

<script>
  function load(url) {
    let xhr = new XMLHttpRequest();
    xhr.open('GET', url);
    xhr.timeout = 1000;
    xhr.send();

    xhr.onload = function() {
      alert(`Loaded: ${this.status} ${this.responseText}`);
    };

    xhr.onerror = () => alert('Error');

    xhr.ontimeout = () => alert('Timeout!');
  }
</script>

<button onclick="load('/article/xmlhttprequest/hello.txt')">Load</button>
<button onclick="load('/article/xmlhttprequest/hello.txt?speed=0')">Load with timeout</button>
<button onclick="load('no-such-page')">Load 404</button>
<button onclick="load('http://example.com')">Load another domain</button>
  1. The first button triggers only onload as it loads the file hello.txt normally.
  2. The second button loads a very slow URL, so it calls only ontimeout (because xhr.timeout is set).
  3. The third button loads a non-existant URL, but it also calls onload (with “Loaded: 404”), because there’s no network error.
  4. The last button tries to load a page from another domain. That’s prohibited unless the remote server explicitly agrees by sending certain headers (to be covered later), so we have onerror here. The onerror handler would also trigger in other cases if we start a request, and then sever the network connection of our device.

Response: status, responseText and others

Once the server has responded, we can receive the result in the following properties of the request object:

status
HTTP status code: 200, 404, 403 and so on. Also can be 0 if an error occured.
statusText
HTTP status message: usually OK for 200, Not Found for 404, Forbidden for 403 and so on.
responseText
The text of the server response,

If the server returns XML with the correct header Content-type: text/xml, then there’s also responseXML property with the parsed XML document. You can query it with xhr.responseXml.querySelector("...") and perform other XML-specific operations.

That’s rarely used, because most of the time JSON is returned by the server. And then we can parse it using JSON.parse(xhr.responseText).

Synchronous and asynchronous requests

If in the open method the third parameter async is set to false, the request is made synchronously.

In other words, Javascript execution pauses at that line and continues when the response is received. Somewhat like alert or prompt commands.

Synchronous calls are used rarely, because they block in-page Javascript till the loading is complete. In some browsers, a user is unable to scroll the page.

// Synchronous request
xhr.open('GET', 'phones.json', false);

// Send it
xhr.send();
// ...JavaScript "hangs" and waits till the request is complete

If a synchronous call takes too much time, the browser may suggest to close the “hanging” webpage.

Also, because of the blocking, it becomes impossible to send two requests simultaneously. And, looking a bit forward, let’s note that some advanced capabilities of XMLHttpRequest, like requesting from another domain or specifying a timeout, are unavailable for synchronous requests.

Because of all that, synchronous requests are used very sparingly, almost never.

By default, requests are asynchronous.

The same request made asynchronously:

let xhr = new XMLHttpRequest();

xhr.open('GET', 'phones.json'); // the third parameter is true by default

xhr.send(); // (1)

xhr.onreadystatechange = function() { // (3)
  if (xhr.readyState != 4) return;

  button.innerHTML = 'Complete!';

  if (xhr.status != 200) {
    alert(xhr.status + ': ' + xhr.statusText);
  } else {
    alert(xhr.responseText);
  }

}

button.innerHTML = 'Loading...'; // (2)
button.disabled = true;

Now as there’s no third argument in open (or if we explicitly set it to true), the request is asynchronous. In other words, after the call xhr.send() in the line (1), Javascript does not “hang”, but continues to execute.

In our case, it means that (2) shows a “loading” message.

Then, after time, when the result is received, it comes in the event handler (3) that we’ll cover a bit later.

The full example in action:

Result
phones.json
server.js
index.html
[
    {
        "age": 0,
        "id": "motorola-xoom-with-wi-fi",
        "imageUrl": "img/phones/motorola-xoom-with-wi-fi.0.jpg",
        "name": "Motorola XOOM\u2122 with Wi-Fi",
        "snippet": "The Next, Next Generation\r\n\r\nExperience the future with Motorola XOOM with Wi-Fi, the world's first tablet powered by Android 3.0 (Honeycomb)."
    },
    {
        "age": 1,
        "id": "motorola-xoom",
        "imageUrl": "img/phones/motorola-xoom.0.jpg",
        "name": "MOTOROLA XOOM\u2122",
        "snippet": "The Next, Next Generation\n\nExperience the future with MOTOROLA XOOM, the world's first tablet powered by Android 3.0 (Honeycomb)."
    },
    {
        "age": 2,
        "carrier": "AT&amp;T",
        "id": "motorola-atrix-4g",
        "imageUrl": "img/phones/motorola-atrix-4g.0.jpg",
        "name": "MOTOROLA ATRIX\u2122 4G",
        "snippet": "MOTOROLA ATRIX 4G the world's most powerful smartphone."
    },
    {
        "age": 3,
        "id": "dell-streak-7",
        "imageUrl": "img/phones/dell-streak-7.0.jpg",
        "name": "Dell Streak 7",
        "snippet": "Introducing Dell\u2122 Streak 7. Share photos, videos and movies together. It\u2019s small enough to carry around, big enough to gather around."
    },
    {
        "age": 4,
        "carrier": "Cellular South",
        "id": "samsung-gem",
        "imageUrl": "img/phones/samsung-gem.0.jpg",
        "name": "Samsung Gem\u2122",
        "snippet": "The Samsung Gem\u2122 brings you everything that you would expect and more from a touch display smart phone \u2013 more apps, more features and a more affordable price."
    },
    {
        "age": 5,
        "carrier": "Dell",
        "id": "dell-venue",
        "imageUrl": "img/phones/dell-venue.0.jpg",
        "name": "Dell Venue",
        "snippet": "The Dell Venue; Your Personal Express Lane to Everything"
    },
    {
        "age": 6,
        "carrier": "Best Buy",
        "id": "nexus-s",
        "imageUrl": "img/phones/nexus-s.0.jpg",
        "name": "Nexus S",
        "snippet": "Fast just got faster with Nexus S. A pure Google experience, Nexus S is the first phone to run Gingerbread (Android 2.3), the fastest version of Android yet."
    },
    {
        "age": 7,
        "carrier": "Cellular South",
        "id": "lg-axis",
        "imageUrl": "img/phones/lg-axis.0.jpg",
        "name": "LG Axis",
        "snippet": "Android Powered, Google Maps Navigation, 5 Customizable Home Screens"
    },
    {
        "age": 8,
        "id": "samsung-galaxy-tab",
        "imageUrl": "img/phones/samsung-galaxy-tab.0.jpg",
        "name": "Samsung Galaxy Tab\u2122",
        "snippet": "Feel Free to Tab\u2122. The Samsung Galaxy Tab\u2122 brings you an ultra-mobile entertainment experience through its 7\u201d display, high-power processor and Adobe\u00ae Flash\u00ae Player compatibility."
    },
    {
        "age": 9,
        "carrier": "Cellular South",
        "id": "samsung-showcase-a-galaxy-s-phone",
        "imageUrl": "img/phones/samsung-showcase-a-galaxy-s-phone.0.jpg",
        "name": "Samsung Showcase\u2122 a Galaxy S\u2122 phone",
        "snippet": "The Samsung Showcase\u2122 delivers a cinema quality experience like you\u2019ve never seen before. Its innovative 4\u201d touch display technology provides rich picture brilliance, even outdoors"
    },
    {
        "age": 10,
        "carrier": "Verizon",
        "id": "droid-2-global-by-motorola",
        "imageUrl": "img/phones/droid-2-global-by-motorola.0.jpg",
        "name": "DROID\u2122 2 Global by Motorola",
        "snippet": "The first smartphone with a 1.2 GHz processor and global capabilities."
    },
    {
        "age": 11,
        "carrier": "Verizon",
        "id": "droid-pro-by-motorola",
        "imageUrl": "img/phones/droid-pro-by-motorola.0.jpg",
        "name": "DROID\u2122 Pro by Motorola",
        "snippet": "The next generation of DOES."
    },
    {
        "age": 12,
        "carrier": "AT&amp;T",
        "id": "motorola-bravo-with-motoblur",
        "imageUrl": "img/phones/motorola-bravo-with-motoblur.0.jpg",
        "name": "MOTOROLA BRAVO\u2122 with MOTOBLUR\u2122",
        "snippet": "An experience to cheer about."
    },
    {
        "age": 13,
        "carrier": "T-Mobile",
        "id": "motorola-defy-with-motoblur",
        "imageUrl": "img/phones/motorola-defy-with-motoblur.0.jpg",
        "name": "Motorola DEFY\u2122 with MOTOBLUR\u2122",
        "snippet": "Are you ready for everything life throws your way?"
    },
    {
        "age": 14,
        "carrier": "T-Mobile",
        "id": "t-mobile-mytouch-4g",
        "imageUrl": "img/phones/t-mobile-mytouch-4g.0.jpg",
        "name": "T-Mobile myTouch 4G",
        "snippet": "The T-Mobile myTouch 4G is a premium smartphone designed to deliver blazing fast 4G speeds so that you can video chat from practically anywhere, with or without Wi-Fi."
    },
    {
        "age": 15,
        "carrier": "US Cellular",
        "id": "samsung-mesmerize-a-galaxy-s-phone",
        "imageUrl": "img/phones/samsung-mesmerize-a-galaxy-s-phone.0.jpg",
        "name": "Samsung Mesmerize\u2122 a Galaxy S\u2122 phone",
        "snippet": "The Samsung Mesmerize\u2122 delivers a cinema quality experience like you\u2019ve never seen before. Its innovative 4\u201d touch display technology provides rich picture brilliance,even outdoors"
    },
    {
        "age": 16,
        "carrier": "Sprint",
        "id": "sanyo-zio",
        "imageUrl": "img/phones/sanyo-zio.0.jpg",
        "name": "SANYO ZIO",
        "snippet": "The Sanyo Zio by Kyocera is an Android smartphone with a combination of ultra-sleek styling, strong performance and unprecedented value."
    },
    {
        "age": 17,
        "id": "samsung-transform",
        "imageUrl": "img/phones/samsung-transform.0.jpg",
        "name": "Samsung Transform\u2122",
        "snippet": "The Samsung Transform\u2122 brings you a fun way to customize your Android powered touch screen phone to just the way you like it through your favorite themed \u201cSprint ID Service Pack\u201d."
    },
    {
        "age": 18,
        "id": "t-mobile-g2",
        "imageUrl": "img/phones/t-mobile-g2.0.jpg",
        "name": "T-Mobile G2",
        "snippet": "The T-Mobile G2 with Google is the first smartphone built for 4G speeds on T-Mobile's new network. Get the information you need, faster than you ever thought possible."
    },
    {
        "age": 19,
        "id": "motorola-charm-with-motoblur",
        "imageUrl": "img/phones/motorola-charm-with-motoblur.0.jpg",
        "name": "Motorola CHARM\u2122 with MOTOBLUR\u2122",
        "snippet": "Motorola CHARM fits easily in your pocket or palm.  Includes MOTOBLUR service."
    }
]
var http = require('http');
var url = require('url');
var querystring = require('querystring');
var static = require('node-static');
var file = new static.Server('.', {
  cache: 0
});


function accept(req, res) {

  if (req.url == '/phones.json') {
    // искусственная задержка для наглядности
    setTimeout(function() {
      file.serve(req, res);
    }, 2000);
  } else {
    file.serve(req, res);
  }

}


// ------ запустить сервер -------

if (!module.parent) {
  http.createServer(accept).listen(8080);
} else {
  exports.accept = accept;
}
<!DOCTYPE HTML>
<html>
<head>
  <meta charset="utf-8">
</head>
<body>

  <button onclick="loadPhones()" id="button">Load phones.json!</button>

  <script>
    function loadPhones() {

      let xhr = new XMLHttpRequest();

      xhr.open('GET', 'phones.json');


      xhr.send();


      xhr.onreadystatechange = function() {
        if (xhr.readyState != 4) return;

        button.innerHTML = 'Complete!';

        if (xhr.status != 200) {
          // handle error
          alert(xhr.status + ': ' + xhr.statusText);
        } else {
          // show result
          alert(xhr.responseText);
        }

      }

      button.innerHTML = 'Loading...';
      button.disabled = true;
    }
  </script>

</body>
</html>

Event “readystatechange”

The event readystatechange occurs multiple times during sending the request and receiving the response.

As the name suggests, there’s a “ready state” of XMLHttpRequest. It is accessible as xhr.readyState.

In the example above we only used state 4 (request complete), but there are few more.

All states, as in the specification:

const unsigned short UNSENT = 0; // initial state
const unsigned short OPENED = 1; // open called
const unsigned short HEADERS_RECEIVED = 2; // response headers received
const unsigned short LOADING = 3; // response is loading (a data packed is received)
const unsigned short DONE = 4; // request complete

An XMLHttpRequest object travels them in the order 0123 → … → 34. State 3 repeats every time a data packet is received over the network.

The example above demostrates these states. The server answers the request digits by sending a string of 1000 digits once per second.

Result
server.js
index.html
var http = require('http');
var url = require('url');
var querystring = require('querystring');
var static = require('node-static');
var file = new static.Server('.');

function accept(req, res) {

  if (req.url == '/digits') {

    res.writeHead(200, {
      'Content-Type': 'text/plain',
      'Cache-Control': 'no-cache'
    });

    var i = 0;

    var timer = setInterval(write, 1000);
    write();

    function write() {
      res.write(new Array(1000).join(++i + '') + ' ');
      if (i == 9) {
        clearInterval(timer);
        res.end();
      }

    }
  } else {
    file.serve(req, res);
  }
}



// ----- запуск accept как сервера из консоли или как модуля ------

if (!module.parent) {
  http.createServer(accept).listen(8080);
} else {
  exports.accept = accept;
}
<!DOCTYPE HTML>
<html>
<head>
  <meta charset="utf-8">
</head>
<body>

  <button onclick="run()">Load digits</button>

  <ul id="log"></ul>

  <script>
    function run() {

      let xhr = new XMLHttpRequest();
      write(xhr.readyState);

      xhr.open('GET', 'digits', true);
      write(xhr.readyState);

      xhr.onreadystatechange = function() {
        write(xhr.readyState + " responseText length:" + xhr.responseText.length);
      };

      xhr.send();
    }

    function write(text) {
      let li = log.appendChild(document.createElement('li'));
      li.innerHTML = text;
    }
  </script>

</body>
</html>
Packets may break at any byte

One might think that readyState=3 (the next data packet is received) allows us to get the current (not full yet) response body in responseText.

That’s true. But only partially.

Technically, we do not have control over breakpoints between network packets. Many languages use multi-byte encodings like UTF-8, where a character is represented by multiple bytes. Some characters use only 1 byte, some use 2 or more. And packets may split in the middle of a character.

E.g. the letter ö is encoded with two bytes. The first of them may be at the end of one packet, and the second one – at the beginning of the next packet.

So, during the readyState, at the end of responseText there will be a half-character byte. That may lead to problems. In some simple cases, when we use only latin characters and digits (they all are encoded with 1 byte), such thing can’t happen, but in other cases, that can become a source of bugs.

HTTP-headers

XMLHttpRequest allows both to send custom headers and read headers from the response.

There are 3 methods for HTTP-headers:

setRequestHeader(name, value)

Sets the request header with the given name and value.

For instance:

xhr.setRequestHeader('Content-Type', 'application/json');
Headers limitations

Several headers are managed exclusively by the browser, e.g. Referer and Host. The full list is in the specification.

XMLHttpRequest is not allowed to change them, for the sake of user safety and correctness of the request.

Can’t remove a header

Another peciliarity of XMLHttpRequest is that one can’t undo setRequestHeader.

Once the header is set, it’s set. Additional calls add information to the header, don’t overwrite it.

For instance:

xhr.setRequestHeader('X-Auth', '123');
xhr.setRequestHeader('X-Auth', '456');

// the header will be:
// X-Auth: 123, 456
getResponseHeader(name)

Gets the response header with the given name (except Set-Cookie and Set-Cookie2).

For instance:

xhr.getResponseHeader('Content-Type')
getAllResponseHeaders()

Returns all response headers, except Set-Cookie and Set-Cookie2.

Headers are returned as a single line, e.g.:

Cache-Control: max-age=31536000
Content-Length: 4260
Content-Type: image/png
Date: Sat, 08 Sep 2012 16:53:16 GMT

The line break between headers is always "\r\n" (doesn’t depend on OS), so we can easily split it into individual headers. The separator between the name and the value is always a colon followed by a space ": ". That’s fixed in the specification.

So, if we want to get an object with name/value pairs, we need to throw in a bit JS.

Like this (assuming that if two headers have the same name, then the latter one overwrites the former one):

let headers = xhr
  .getAllResponseHeaders()
  .split('\r\n')
  .reduce((result, current) => {
    let [name, value] = current.split(': ');
    result[name] = value;
    return acc;
  }, {});

Timeout

The maximum duration of an asynchronous request can be set using the timeout property:

xhr.timeout = 30000; // 30 seconds (in milliseconds)

If the request exceeds that time, it’s aborted, and the timeout event is generated:

xhr.ontimeout = function() {
  alert( 'Sorry, the request took too long.' );
}

The full event list

The modern specification lists following events (in the lifecycle order):

  • loadstart – the request has started.
  • progress – a data packet of the response has arrived, the whole response body at the moment is in responseText.
  • abort – the request was canceled by the call xhr.abort().
  • error – connection error has occured, e.g. wrong domain name. Doesn’t happen for HTTP-errors like 404.
  • load – the request has finished successfully.
  • timeout – the request was canceled due to timeout (only happens if it was set).
  • loadend – the request has finished (succeffully or not).

The most used events are load completion (onload), load failure (onerror), and also onprogress to track the progress.

We’ve already seen another event: readystatechange. Historically, it appeared long ago, before the specification settled. Nowadays, there’s no need to use it, we can replace it with newer events, but it can often be found in older scripts.

Summary

Typical code of the GET-request with XMLHttpRequest:

let xhr = new XMLHttpRequest();

xhr.open('GET', '/my/url');

xhr.send();

xhr.onload = function() {
  // we can check
  // status, statusText - for response HTTP status
  // responseText, responseXML (when content-type: text/xml) - for the response

  if (this.status != 200) {
    // handle error
    alert( 'error: ' + this.status);
    return;
  }

  // get the response from this.responseText
};

xhr.onerror = function() {
  // handle error
};

XMLHttpRequest is widely used, but there’s a more modern method named fetch(url) that returns a promise, thus working well with async/await. We’ll cover it soon in the next sections.

Tutorial map

Comments

read this before commenting…
  • You're welcome to post additions, questions to the articles and answers to them.
  • To insert a few words of code, use the <code> tag, for several lines – use <pre>, for more than 10 lines – use a sandbox (plnkr, JSBin, codepen…)
  • If you can't understand something in the article – please elaborate.