W3docs

XMLHttpRequest

Learn how to use XMLHttpRequest (XHR) in JavaScript to send asynchronous HTTP requests: the open and send methods, readyState, and events.

JavaScript is an essential programming language for web development, enabling dynamic and interactive user experiences. One of the key features of JavaScript is its ability to communicate with servers, retrieve data, and update web pages asynchronously. This is primarily achieved through the use of XMLHttpRequest (XHR). This article provides a deep dive into XMLHttpRequest, including its methods, properties, and practical applications, complete with multiple code examples to facilitate learning.

Info

XMLHttpRequest works with callback functions. For new code you will usually reach for the Fetch API instead, which is promise-based and works cleanly with async/await. XHR is still worth understanding: you will meet it in older codebases, and it remains the only built-in API that reports fine-grained upload progress.

This page covers what an XHR object is, how to configure and send a request with open and send, how to read the response through readyState and the load/error/timeout events, how to parse JSON, how to POST data, how to abort a request, and how XHR compares with fetch.

Understanding XMLHttpRequest

XMLHttpRequest (XHR) is a built-in browser object that lets JavaScript send an HTTP or HTTPS request to a server and receive the response without reloading the page. The "XML" in the name is historical, XHR can transfer any text or binary format, and JSON is by far the most common today. This ability to talk to a server in the background is the foundation of what used to be called AJAX (Asynchronous JavaScript and XML).

The lifecycle of a request is always the same: create the object, open it (configure method and URL), attach event handlers to react to the result, then send it.

Creating an XMLHttpRequest Object

First create an instance:

const xhr = new XMLHttpRequest();

A single XMLHttpRequest instance handles one request. To make a second request, create a new object.

Making an HTTP Request

Once the object exists, configure it with open, then dispatch it with send.

The open Method

open initializes a request but does not send it yet. It takes several parameters:

xhr.open(method, url, async, user, password);
  • method: The HTTP method to use, e.g. 'GET' or 'POST'.
  • url: The URL the request is sent to.
  • async: A boolean indicating whether the request is asynchronous. Defaults to true, and you should almost always leave it that way (see the warning below).
  • user: Optional user name for HTTP authentication.
  • password: Optional password for HTTP authentication.

Example:

xhr.open('GET', 'https://jsonplaceholder.typicode.com/posts/1', true);
Warning

Synchronous requests (xhr.open(method, url, false)) freeze the page until the response arrives and are deprecated on the main thread. Always keep async set to true.

The send Method

send dispatches the request to the server. Any event handlers must be attached before calling it. For a GET request, call it without arguments. For a POST, pass the request body as the argument.

Example of a GET request:

xhr.send();

Example of a POST request with form-encoded data:

xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send('param1=value1&param2=value2');

The setRequestHeader method adds an HTTP header to the outgoing request, and must be called after open but before send.

Handling Server Responses

To handle responses from the server, various event listeners can be used.

The onreadystatechange Event

The onreadystatechange event is triggered whenever the readyState property changes. The readyState property holds the status of the XMLHttpRequest.

  • 0: UNSENT
  • 1: OPENED
  • 2: HEADERS_RECEIVED
  • 3: LOADING
  • 4: DONE

A request is finished and successful only when readyState is 4 (DONE) and the HTTP status is in the success range (typically 200). Checking only readyState === 4 is a common bug, because the server may have answered with 404 or 500.

Example:

javascript— editable
Note

While onreadystatechange works, modern code generally prefers onload and onerror for simpler, more readable request handling. onreadystatechange is mainly used when you need to track intermediate states (like progress or headers received).

The load Event

The load event fires once the response has fully arrived. It is simpler than onreadystatechange because you do not have to test readyState yourself, it only fires at the DONE stage. You still check status to distinguish a real success from an HTTP error.

Example:

javascript— editable

The progress Event

For large downloads you can report progress with the progress event. When the server sends a Content-Length header the event is determinate (lengthComputable is true) and you can compute a percentage:

xhr.onprogress = function(event) {
  if (event.lengthComputable) {
    const percent = Math.round((event.loaded / event.total) * 100);
    console.log(`Downloaded ${percent}%`);
  }
};

To track an upload instead, attach handlers to xhr.upload (xhr.upload.onprogress). The upload progress object is the one feature Fetch still cannot fully replicate.

Handling Errors

Robust code must handle failures. Two events cover the failure cases:

  • onerror fires on a network-level failure, the request never reached the server, DNS failed, CORS blocked it, etc. Note that an HTTP 404 or 500 is not a network error: it triggers load, not error, so you must still inspect status.
  • ontimeout fires if the request takes longer than xhr.timeout milliseconds. A timeout of 0 (the default) means no limit.

Example:

javascript— editable

Parsing JSON Responses

Server responses are most often JSON. The simplest approach is to read the raw text from xhr.responseText and parse it yourself with JSON.parse:

javascript— editable

Alternatively, set xhr.responseType = 'json' before sending, and the browser parses the body for you. The parsed value is then available on xhr.response (not xhr.responseText):

const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://jsonplaceholder.typicode.com/posts/1', true);
xhr.responseType = 'json';

xhr.onload = function() {
  if (xhr.status === 200) {
    console.log('title: ' + xhr.response.title); // already an object
  }
};

xhr.send();

responseType also accepts 'text', 'blob', 'arraybuffer', and 'document' for non-JSON payloads.

Sending Data with POST

To send a JSON body, set the Content-Type header and stringify your object with JSON.stringify:

const xhr = new XMLHttpRequest();
xhr.open('POST', 'https://jsonplaceholder.typicode.com/posts', true);
xhr.setRequestHeader('Content-Type', 'application/json');

xhr.onload = function() {
  if (xhr.status === 201) { // 201 Created
    console.log('Created:', xhr.responseText);
  }
};

xhr.send(JSON.stringify({ title: 'foo', body: 'bar', userId: 1 }));

For traditional form submissions, send a FormData object instead, the browser sets the correct multipart Content-Type automatically, so do not call setRequestHeader for it.

Aborting a Request

Call xhr.abort() to cancel an in-flight request, for example when the user navigates away or types a newer search query. After aborting, the abort event fires instead of load:

const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://jsonplaceholder.typicode.com/posts', true);
xhr.onabort = () => console.log('Request was cancelled');
xhr.send();

// Later, cancel it:
xhr.abort();

The Fetch equivalent uses an AbortController.

XMLHttpRequest vs Fetch

XMLHttpRequestFetch
Programming modelCallbacks / eventsPromises, works with await
Upload progressYes (xhr.upload)No
Download progressYes (progress event)Via streams (more code)
Abortxhr.abort()AbortController
Rejects on HTTP errorNo, you check statusNo, you check response.ok

For most new code prefer Fetch. Reach for XHR when you need granular upload-progress reporting or must support a very old environment.

Conclusion

XMLHttpRequest lets JavaScript exchange data with a server in the background: you create the object, open it, attach load/error/timeout handlers, and send it. Remember to check both readyState and status, parse JSON yourself or via responseType, and use abort() to cancel stale requests. For most new code the promise-based Fetch API is the better default, but understanding XHR keeps you fluent in older codebases and the cases, like upload progress, where it still wins.

Practice

Practice
Which of the following statements about XMLHttpRequest are correct?
Which of the following statements about XMLHttpRequest are correct?
Was this page helpful?