← 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.
Syntax
javascript
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
  if (this.readyState == 4 && this.status == 200) {
    // this.responseText
  }
};
xhr.open("GET", url);
xhr.send();

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.

उदाहरण: 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.

उदाहरण: The readyState Lifecycle

javascript
// Declare the constant `xhr` as a new `XMLHttpRequest` instance
// Declare the constant `xhr` as a new `XMLHttpRequest` instance
const xhr = new XMLHttpRequest();
// Assign `() => console.log("readyState:", xhr.readyState)` to `xhr.onreadystatechange`
// Assign `() => console.log("readyState:", xhr.readyState)` to `xhr.onreadystatechange`
xhr.onreadystatechange = () => console.log("readyState:", xhr.readyState);
// Call `xhr.open("GET", "data.php")`
// Call `xhr.open("GET", "data.php")`
xhr.open("GET", "data.php");
// Call `xhr.send()`
// Call `xhr.send()`
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.

उदाहरण: Checking the HTTP Status Code

javascript
// Declare the constant `xhr` as a new `XMLHttpRequest` instance
// Declare the constant `xhr` as a new `XMLHttpRequest` instance
const xhr = new XMLHttpRequest();
// Assign `() => {` to `xhr.onreadystatechange`
// Assign `() => {` to `xhr.onreadystatechange`
xhr.onreadystatechange = () => {
  // Check whether `xhr.readyState === 4`
  // Check whether `xhr.readyState === 4`
  if (xhr.readyState === 4) {
    // Print `xhr.status === 200 ? "Success" : "Failed with status " + xhr.status` to the console
    // Print `xhr.status === 200 ? "Success" : "Failed with status " + xhr.status` to the console
    console.log(xhr.status === 200 ? "Success" : "Failed with status " + xhr.status);
  }
};
// Call `xhr.open("GET", "data.php")`
// Call `xhr.open("GET", "data.php")`
xhr.open("GET", "data.php");
// Call `xhr.send()`
// Call `xhr.send()`
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.

उदाहरण: Sending the Request and Handling Errors

javascript
// Declare the constant `xhr` as a new `XMLHttpRequest` instance
// Declare the constant `xhr` as a new `XMLHttpRequest` instance
const xhr = new XMLHttpRequest();
// Assign `() => console.log("Loaded:", xhr.responseText)` to `xhr.onload`
// Assign `() => console.log("Loaded:", xhr.responseText)` to `xhr.onload`
xhr.onload = () => console.log("Loaded:", xhr.responseText);
// Assign `() => console.log("Request failed")` to `xhr.onerror`
// Assign `() => console.log("Request failed")` to `xhr.onerror`
xhr.onerror = () => console.log("Request failed");
// Call `xhr.open("GET", "data.php")`
// Call `xhr.open("GET", "data.php")`
xhr.open("GET", "data.php");
// Call `xhr.send()`
// Call `xhr.send()`
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.

उदाहरण: XMLHttpRequest vs fetch: A Direct Comparison

javascript
// Declare the constant `xhr` as a new `XMLHttpRequest` instance
// Declare the constant `xhr` as a new `XMLHttpRequest` instance
const xhr = new XMLHttpRequest();
// Assign `() => console.log(xhr.responseText)` to `xhr.onload`
// Assign `() => console.log(xhr.responseText)` to `xhr.onload`
xhr.onload = () => console.log(xhr.responseText);
// Call `xhr.open("GET", "data.php")`
// Call `xhr.open("GET", "data.php")`
xhr.open("GET", "data.php");
// Call `xhr.send()`
// Call `xhr.send()`
xhr.send();

// Call `fetch("data.php").then(r => r.text()).then(text => console.log(text))`
// Call `fetch("data.php").then(r => r.text()).then(text => console.log(text))`
fetch("data.php").then(r => r.text()).then(text => console.log(text));
Live Example
Related Topics
{# common_mistakes/chapter_summary/browser_support: on Hindi pages the view already swaps in the hi_ translation fields (or blanks these out if untranslated), so this renders correctly for both languages without a lang_code check here. #}

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.