← Back to JavaScript Course | Chapter 9: Async & Web APIs | Lesson 12 of 26

JS AJAX XMLHttp

The XMLHttpRequest object is the original browser API for making AJAX requests, and while fetch() is preferred for new code, understanding XMLHttpRequest's readyState-based lifecycle remains useful for maintaining older code and understanding how browsers implement HTTP requests under the hood.

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

javascript
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

javascript
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

javascript
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

javascript
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

javascript
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));
Common Mistakes
  1. Checking readyState without also checking status, which can trigger the completion handler on a failed request (like a 404) as if it succeeded.
  2. Calling .open() with the wrong argument order or forgetting to call .send() afterward, leaving the request configured but never actually sent.
  3. Setting request headers with .setRequestHeader() before calling .open(), when it must be called after open() but before send().
Chapter Summary
  • 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.
Browser Support

XMLHttpRequest has been supported in every browser since the early 2000s and remains supported today for backward compatibility.

Login to run this code

C/C++/Java/PHP execution requires a free account. Your code is saved — you'll land right back in the editor after logging in.