cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

XMLHttpRequest

Learn the older browser request API: the XMLHttpRequest lifecycle, readyState events, progress events, response types, and when XHR is still useful in legacy JavaScript.

What you’ll be able to do
  • 01
    Read the lifecycleFollow open(), send(), readyState changes, and the final event order.
  • 02
    Handle success and failure honestlyTell HTTP errors apart from network failures, aborts, and timeouts.
  • 03
    Choose XHR on purposeRecognize upload progress and legacy-code cases where XHR still appears.

Why learn XMLHttpRequest?

XMLHttpRequest, usually shortened to XHR, is the older browser API for making HTTP requests from JavaScript. Modern code often uses fetch, but XHR is still worth knowing because it appears in legacy apps, browser libraries, upload progress widgets, and old examples you will meet at work.

The name sounds huge, but the idea is small: create a request object, configure it, attach event handlers, then send it. Instead of returning a promise, XHR talks back through events and a numeric readyState.

Real-life analogyXHR is an old rotary phone

Think of XHR as a rotary phone. It works, it has tactile knobs and dials, and it teaches you what is happening during the call. Fetch is the smartphone: more comfortable for everyday use, especially when your code already thinks in promises.

In real life: A rotary phone can still call people
In JavaScript: XHR can still request data in every browser
In real life: It has dials, clicks, and steps
In JavaScript: XHR has readyState, readystatechange, progress, timeout, and abort events
In real life: You find it in older houses
In JavaScript: You find XHR in older codebases and libraries
In real life: A smartphone is easier for most new calls
In JavaScript: fetch is usually cleaner for new request code

Where the analogy stops: A rotary phone and smartphone use different networks; XHR and fetch both use the browser’s networking rules. The analogy is about ergonomics and age, not different internet pipes.

You will run real requests in this lesson, but never to external URLs. The labs request this page, a missing same-origin path, generated blob: URLs, or a generated upload body. That keeps the lesson safe on a static site.

The shape to memorize

open(method, url, async) prepares the request. setRequestHeader() happens after open() and before send(). send(body) starts the request. Events tell you what happened.

The XHR lifecycle

INTERACTIVE

The lifecycle has two parts: the code you write and the events the browser fires. Your code runs in this order:

The request you writePop out in the code editor (opens in a new tab)JavaScript
const xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.timeout = timeoutMs;
xhr.addEventListener("readystatechange", record);
xhr.addEventListener("loadstart", record);
xhr.addEventListener("progress", record);
xhr.addEventListener("load", record);
xhr.addEventListener("error", record);
xhr.addEventListener("abort", record);
xhr.addEventListener("timeout", record);
xhr.addEventListener("loadend", record);
xhr.send();

The important surprise is that open() does not send the request. It stores the method and URL and changes readyState to OPENED. The trip begins only when send() runs.

XHR lifecycle playground
Lifecycle codePop out in the code editor (opens in a new tab)JavaScript
const xhr = new XMLHttpRequest();xhr.open("GET", targetUrl);xhr.timeout = target === "timeout" ? 1 : 0;xhr.responseType = "text";for (const name of ["loadstart", "progress", "load", "error", "abort", "timeout", "loadend"]) {  xhr.addEventListener(name, logEvent);}xhr.addEventListener("readystatechange", logReadyState);xhr.send();if (target === "abort") setTimeout(() => xhr.abort(), 0);
Live event log

No request has run yet.

Try it yourself

Choose a target, then send a real same-origin XHR.

Every option stays inside this site: the page itself, a missing local path, or generated blob URLs.

Try “Missing page.” You should see a real HTTP status like 404, but the final event is still load. That is because XHR separates transport success from application success. A server saying “not found” is still a response. A revoked blob URL, timeout, abort, or CORS failure gives no usable HTTP response and normally leaves status at 0.

readyState and events

STEP THROUGH

XHR has five ready states. They are exposed as numbers and as constants on XMLHttpRequest, such as XMLHttpRequest.DONE. People usually write the names in comments or logs because the numbers alone are hard to remember.

Real-life analogyreadyState is a pizza order

A pizza order moves through recognizable stages. XHR does too: not started, opened, headers, loading, done. The names make event logs readable.

In real life: 0: menu in your hand
In JavaScript: UNSENT: the object exists but is not opened
In real life: 1: you dialed the shop
In JavaScript: OPENED: method and URL are set
In real life: 2: the shop picked up
In JavaScript: HEADERS_RECEIVED: status and headers are known
In real life: 3: the pizza is on the way, slice by slice
In JavaScript: LOADING: response bytes are arriving
In real life: 4: delivered, even if the order is wrong
In JavaScript: DONE: finished, failed, timed out, or aborted

Where the analogy stops: A pizza shop usually gives one final answer. XHR may fire many progress events during the trip, and a DONE request may still have status 0.

XHR readyState values
ValueConstantWhat it meansWhat you can usually read
0UNSENTCreated but not openedConfiguration only
1OPENEDOpened but not sent or not answered yetConfiguration only
2HEADERS_RECEIVEDHeaders and status are availablestatus and response headers
3LOADINGBody is arrivingPartial text in some cases and progress
4DONEThe request is completestatus, headers, and response

The event order is not “one event per ready state.” It is a mix: readystatechange for state changes, loadstart and progress for byte movement, one final outcome event (load, error, abort, or timeout), then loadend for cleanup.

Step through XHR event order
Step 0 of 12Ready
Your turn: follow the blue line

Step through the modeled success order. It matches the browser event shape checked for this lesson: a start, zero or more progress/state changes, one final outcome, then loadend.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
xhr.open("GET", url);xhr.timeout = timeoutMs;xhr.addEventListener("readystatechange", record);xhr.addEventListener("loadstart", record);xhr.addEventListener("progress", record);xhr.addEventListener("load", record);xhr.addEventListener("error", record);xhr.addEventListener("abort", record);xhr.addEventListener("timeout", record);xhr.addEventListener("loadend", record);xhr.send();
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Responses, status, and responseType

INTERACTIVE

XHR gives you several response tools. status is the numeric HTTP status. statusText is the browser’s text for that status when available. responseURL is the final URL after redirects. getAllResponseHeaders() returns the exposed response headers as one string.

For the body, prefer xhr.response. Its type depends on responseType. responseText is only safe for the default empty string or "text". If you set "json", "blob", "arraybuffer", or "document", read response instead.

responseType lab
Changing responseTypePop out in the code editor (opens in a new tab)JavaScript
const xhr = new XMLHttpRequest();xhr.open("GET", location.href);xhr.responseType = selectedType; // "", "text", "json", "blob", "arraybuffer", or "document"xhr.onload = () => {  console.log(xhr.status);  console.log(xhr.responseType || "text default");  console.log(typeof xhr.response);};xhr.send();
Response summary

Run the request to inspect xhr.response.

Try it yourself

Run the request to inspect xhr.response.

The same same-origin page is requested each time. json becomes null for this HTML page; document parses the HTML into a Document in browsers that support it.
XHR response fields people confuse
FeatureUse it forWatch out
responseTypeTell the browser how to expose the bodySet it before the response finishes; not every type is useful for every URL.
responseThe parsed body: string, object, Blob, ArrayBuffer, Document, or nullFor json, invalid JSON becomes null instead of throwing in onload.
responseTextText bodies onlyReading it with non-text responseType can throw.
overrideMimeType()Force a MIME type interpretation before sendingUse sparingly; it does not change what the server actually sent.
withCredentialsInclude cookies/auth on cross-origin requests when allowedThe server must opt in with CORS credentials headers.

Upload progress: XHR’s superpower

INTERACTIVE
Real-life analogyUpload progress is a moving truck

Download progress says “bytes are arriving.” Upload progress says “bytes are leaving.” If you are sending a big file and want a real progress bar, XHR still has a simple event target for it.

In real life: Boxes being loaded one by one
In JavaScript: Bytes leaving the browser
In real life: A clipboard that counts loaded boxes
In JavaScript: xhr.upload.onprogress
In real life: The destination may still reject the delivery
In JavaScript: The server may return 404 or 405 after upload events

Where the analogy stops: A moving truck can always count boxes. Browsers, networks, and local servers may coalesce small uploads so you might see few or no progress events for tiny bodies.

The lab sends a generated text Blob to this same page with POST. A static site might reject the method after the browser tries to upload the body, and that is okay for the lesson: we are watching upload events, not storing a file.

Upload progress meter
Upload progress codePop out in the code editor (opens in a new tab)JavaScript
const xhr = new XMLHttpRequest();xhr.open("POST", location.href);xhr.upload.onprogress = (event) => {  const percent = event.lengthComputable    ? Math.round((event.loaded / event.total) * 100)    : "unknown";  console.log("uploaded", percent);};xhr.onloadend = () => console.log("finished", xhr.status);xhr.send(generatedBlob);
Upload log
0%
  1. Click Upload to send a generated Blob to this same page.
Try it yourself

Click Upload to send a generated Blob to this same page.

XHR has an upload event target. Fetch does not have standard upload progress events, even though it can stream responses.

The modern fetch API can show download progress through response streams, but it does not provide standard upload progress events like xhr.upload.onprogress. That single difference is the main reason many real upload widgets still use XHR.

Wrapping XHR in a promise

PRACTICAL

XHR was designed before promises were the default style. You can wrap it so the outside of your app uses then, catch, and await, while the inside still listens to XHR events. This is the same idea you learned in the Promisification & promise-based APIs lesson.

Promise wrapper around XHRPop out in the code editor (opens in a new tab)JavaScript
function xhrRequest(url, options = {}) {  return new Promise((resolve, reject) => {    const xhr = new XMLHttpRequest();    xhr.open(options.method ?? "GET", url, true);    xhr.responseType = options.responseType ?? "";    xhr.onload = () => resolve({ status: xhr.status, response: xhr.response });    xhr.onerror = () => reject(new TypeError("Network error"));    xhr.ontimeout = () => reject(new DOMException("Timed out", "TimeoutError"));    xhr.onabort = () => reject(new DOMException("Aborted", "AbortError"));    for (const [name, value] of Object.entries(options.headers ?? {})) {      xhr.setRequestHeader(name, value);    }    xhr.send(options.body ?? null);  });}

Notice the policy choice: this wrapper resolves for every HTTP response, including 404. It rejects for transport failures: network error, timeout, or abort. That mirrors how XHR events work and keeps status checking explicit.

Headers and credentials must be configured before sendPop out in the code editor (opens in a new tab)JavaScript
const xhr = new XMLHttpRequest();
xhr.open("POST", "/api/messages", true);
xhr.responseType = "json";
xhr.timeout = 5000;
xhr.withCredentials = true;
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send(JSON.stringify({ text: "Hello from XHR" }));
Do not demo synchronous XHR

XHR has an old async = false mode, but synchronous XHR on the main thread is deprecated because it blocks the page: no clicks, no painting, no helpful feedback. Learn to recognize it in old code, then replace it with asynchronous code.

When XHR is still useful

SORT

New request code usually starts with fetch because it is promise-based and works naturally with AbortSignal, request and response objects, and streams. XHR remains useful in three practical places:

  • Upload progress: use xhr.upload.onprogress for file upload bars.
  • Legacy code: older jQuery-style AJAX, older library versions, and browser support layers often wrap XHR.
  • Fine-grained lifecycle debugging: ready states and events make old behavior easier to inspect.
XHR or fetch?
  • Show a progress bar while a file is being uploaded
  • Read a response stream chunk by chunk
  • Handle a request inside a service worker
  • Maintain legacy jQuery AJAX code
  • Cancel with AbortSignal
  • Load same-origin JSON and parse it
  • Send a request with custom headers
Try it yourself
0 of 7 correct

Sort each real-world request job by the API you would reach for first.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common misconceptions

  • “XHR is gone.” It is old, not gone. Browsers still support it because the web depends on old pages continuing to work.
  • “404 fires error.” No. HTTP error statuses still fire load. Check status.
  • “status 0 means HTTP 0.” No. It means there is no usable HTTP response, often because of abort, timeout, network failure, or CORS.
  • “progress always knows the total.” No. lengthComputable can be false, so show bytes without a percent.
  • “responseText always works.” No. For non-text response types, use response.
  • “Synchronous XHR is a handy shortcut.” It blocks the page and is deprecated on the main thread. Do not use it in new code.

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upName the final readyState

Predict what the program prints.

Starter codePop out in the code editor (opens in a new tab)JavaScript
const states = [0, 1, 2, 3, 4];
const names = ["UNSENT", "OPENED", "HEADERS_RECEIVED", "LOADING", "DONE"];
console.log(names[states.at(-1)]);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upHTTP error or network error?

    Predict the event name and status text this simplified program logs.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    const status = 404;
    const event = status >= 400 ? "load" : "load";
    console.log(event + " " + status);

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeFind the bug: setting a header too early

      This code has the right pieces in the wrong order. Fix the sequence.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const xhr = new XMLHttpRequest();
      xhr.setRequestHeader("Content-Type", "application/json");
      xhr.open("POST", "/api/messages");
      xhr.send(JSON.stringify({ text: "hi" }));
        Exercise 4 · PracticeWrite a cleanup handler

        Write the handler you would add before send().

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        const xhr = new XMLHttpRequest();
        showSpinner();
        xhr.open("GET", location.href);
        // Add one handler that hides the spinner for success, error, abort, and timeout.
        xhr.send();
          Exercise 5 · ChallengeDesign an upload status message

          Write code for a progress message that still works when the browser does not know the total upload size.

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          xhr.upload.onprogress = (event) => {
            // If total length is known, show a percent.
            // Otherwise show raw bytes uploaded.
          };

            Check your understanding

            7 QUESTIONS
            XMLHttpRequest quiz · 7 questionsScore: first tries count
            1. Question 1 of 7Which pair is the basic XHR send sequence?

              Choose an answer to see the explanation.

            2. Question 2 of 7What does this readyState lookup print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const states = [0, 1, 2, 3, 4];
              const names = ["UNSENT", "OPENED", "HEADERS_RECEIVED", "LOADING", "DONE"];
              console.log(names[states.at(-1)]);

              Choose an answer to see the explanation.

            3. Question 3 of 7A server returns HTTP 404 to an XHR. Which final event should your handler expect?

              Choose an answer to see the explanation.

            4. Question 4 of 7Which event does this list end with?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const events = ["loadstart", "progress", "load", "loadend"];
              console.log(events.at(-1));

              Choose an answer to see the explanation.

            5. Question 5 of 7Why can xhr.status be 0?

              Choose an answer to see the explanation.

            6. Question 6 of 7Which XHR property is useful for a real upload progress bar?

              Choose an answer to see the explanation.

            7. Question 7 of 7When can you read xhr.responseText?

              Choose an answer to see the explanation.

            Key takeaways

            • XHR is old but still supported and common in legacy browser code.
            • The sequence is open(), optional configuration and headers, then send().
            • readyState moves from 0 to 4; loadend is the final cleanup event.
            • HTTP error statuses fire load; network failures, aborts, and timeouts use their own events and often leave status at 0.
            • Use xhr.upload.onprogress when you need real upload progress events.

            Final definition: XMLHttpRequest is the browser’s older event-based HTTP request object, useful to understand for ready states, progress events, and legacy network code.

            Up next: Cookies.

            CompleteFrontend Clear concepts. Working examples.