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.
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 totrue, 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);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¶m2=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: UNSENT1: OPENED2: HEADERS_RECEIVED3: LOADING4: 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:
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:
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:
onerrorfires on a network-level failure, the request never reached the server, DNS failed, CORS blocked it, etc. Note that an HTTP404or500is not a network error: it triggersload, noterror, so you must still inspectstatus.ontimeoutfires if the request takes longer thanxhr.timeoutmilliseconds. A timeout of0(the default) means no limit.
Example:
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:
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
| XMLHttpRequest | Fetch | |
|---|---|---|
| Programming model | Callbacks / events | Promises, works with await |
| Upload progress | Yes (xhr.upload) | No |
| Download progress | Yes (progress event) | Via streams (more code) |
| Abort | xhr.abort() | AbortController |
| Rejects on HTTP error | No, you check status | No, 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.