JS AJAX XMLHttp
In this page:
Creating and Configuring a Request
new XMLHttpRequest() creates a new request object, and .open(method, url, async) configures it with an HTTP method, target URL, and whether it should run asynchronously (almost always true in modern code) -- configuration only, nothing is sent yet at this point.
Note: Always pass true (or omit the third argument, since it defaults to true) for asynchronous requests -- the synchronous mode is deprecated and blocks the page.
Warning: Calling .open() more than once on the same XMLHttpRequest object reconfigures it entirely, effectively abandoning any previous configuration.
Example: Creating and Configuring a Request
const xhr = new XMLHttpRequest();
xhr.open("GET", "data.php", true); // configured, nothing sent yet
console.log(xhr.readyState); // 1 - opened
The readyState Lifecycle
readyState progresses through five numeric values as a request proceeds: 0 (uninitialized), 1 (opened), 2 (headers received), 3 (loading), and 4 (done) -- the onreadystatechange event fires each time this value changes, letting you react at any stage, though checking specifically for 4 is the most common pattern.
Note: Check readyState === 4 to detect that the request has fully completed, since intermediate states rarely need separate handling in typical code.
Warning: onreadystatechange fires multiple times throughout a request's lifecycle -- code inside it must check the specific readyState value, or it will run prematurely at an earlier stage.
Example: The readyState Lifecycle
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => console.log("readyState:", xhr.readyState);
xhr.open("GET", "data.php");
xhr.send();
Checking the HTTP Status Code
Once readyState reaches 4, the status property holds the actual HTTP status code returned by the server -- 200 for success, 404 for not found, 500 for a server error -- and checking it alongside readyState is essential to distinguish a truly successful response from a completed-but-failed one.
Note: Always check xhr.status (typically for the 200-299 range) alongside readyState === 4, since a completed request can still represent an HTTP-level failure.
Warning: Treating readyState === 4 alone as "success" is a common bug -- a 404 or 500 response also reaches readyState 4, just with a status code indicating failure.
Example: Checking the HTTP Status Code
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
console.log(xhr.status === 200 ? "Success" : "Failed with status " + xhr.status);
}
};
xhr.open("GET", "data.php");
xhr.send();
Sending the Request and Handling Errors
.send() actually transmits the configured request -- with an optional body argument for POST/PUT requests -- and separate onload/onerror event handlers (a somewhat more modern addition to XMLHttpRequest) offer a cleaner alternative to manually checking readyState for basic success/failure handling.
Note: Consider using .onload and .onerror instead of .onreadystatechange for simpler code, when you only need to react to the final success or failure outcome.
Warning: .onerror fires only for network-level failures (like the request never reaching the server) -- an HTTP error status like 500 still triggers .onload, not .onerror, since the request technically completed.
Example: Sending the Request and Handling Errors
const xhr = new XMLHttpRequest();
xhr.onload = () => console.log("Loaded:", xhr.responseText);
xhr.onerror = () => console.log("Request failed");
xhr.open("GET", "data.php");
xhr.send();
XMLHttpRequest vs fetch: A Direct Comparison
Placing the same GET request side by side highlights fetch's advantage clearly: fewer lines, no manual readyState checking, and native promise chaining -- the reason fetch has become the default recommendation for new code, while XMLHttpRequest knowledge remains useful mainly for legacy code.
Note: When encountering XMLHttpRequest in an existing codebase, understand it well enough to maintain it, but write any brand-new AJAX code with fetch instead.
Warning: Rewriting large amounts of working XMLHttpRequest code to fetch purely for style, without a real functional need, can introduce regressions for no concrete benefit -- weigh the actual value of a migration.
Example: XMLHttpRequest vs fetch: A Direct Comparison
const xhr = new XMLHttpRequest();
xhr.onload = () => console.log(xhr.responseText);
xhr.open("GET", "data.php");
xhr.send();
fetch("data.php").then(r => r.text()).then(text => console.log(text));
- Checking readyState without also checking status, which can trigger the completion handler on a failed request (like a 404) as if it succeeded.
- Calling .open() with the wrong argument order or forgetting to call .send() afterward, leaving the request configured but never actually sent.
- Setting request headers with .setRequestHeader() before calling .open(), when it must be called after open() but before send().
- new XMLHttpRequest() creates a request object, .open(method, url) configures it, and .send() actually fires it off.
- readyState tracks the request's lifecycle stage from 0 (uninitialized) to 4 (complete), typically checked inside onreadystatechange.
- status holds the HTTP status code (like 200 or 404) once the response arrives, and should be checked alongside readyState.
XMLHttpRequest has been supported in every browser since the early 2000s and remains supported today for backward compatibility.
Chapter Quiz — Complete all 26 topics to unlock
0/26 topics done
Complete these topics first:
- JS JSON
- JS Regular Expressions
- JS Fetch API
- JS LocalStorage and SessionStorage
- JS Cookies
- JS setTimeout and setInterval
- JS Event Loop
- JS Web Workers
- JS Service Workers
- JS AJAX
- JS AJAX Intro
- JS AJAX XMLHttp
- JS AJAX Request
- JS AJAX Response
- JS AJAX XML
- JS AJAX PHP
- JS AJAX Database
- JS JSONP
- JS RegExp Flags
- JS RegExp Classes
- JS RegExp Metachars
- JS RegExp Assertions
- JS RegExp Groups
- JS RegExp Quantifiers
- JS JSON HTML
- JS JSON vs XML