completefrontend
Open lab My progress
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
    Follow a request step by stepFollow open(), send(), the readyState numbers, and the order of the events.
  • 02
    Tell an error answer from no answerTell a 404 answer apart from a network failure, an abort, and a timeout.
  • 03
    Know when XHR is still the right toolSpot the places where XHR still appears: upload progress bars and older code.

Why learn XMLHttpRequest?

An HTTP request is a message your page sends to a server. It asks for data, or it sends data. XMLHttpRequest is an object the browser gives you for sending HTTP requests from JavaScript. People shorten its name to XHR.

XHR is the older way to send requests. Newer code usually uses fetch(). You still need to know XHR, because you will meet it in these places:

  • Older apps that were written before fetch() existed.
  • Older libraries, such as old jQuery code, that use XHR inside.
  • Upload progress bars, because XHR can report how much of a file has been sent.

The name sounds big, but the steps are small:

  1. Create a request object.
  2. Open it with a method, such as GET, and an address (a URL).
  3. Add listeners for its events. A listener is a function that runs when its event happens.
  4. Send it.

XHR doesn't give you a promise. It tells you what happened in two ways: through events, and through a number called readyState.

Real-life analogyXHR is an old rotary phone

An old rotary phone still makes calls. You turn the dial, hear each click, and wait for the call to go through. You can see every step. A smartphone does the same job with less work.

In real life: A rotary phone can still make calls
In JavaScript: XHR still works in every browser
In real life: You turn the dial, hear each click, and wait for each step
In JavaScript: XHR shows each step: readyState, readystatechange, and the progress, timeout, and abort events
In real life: You find it in older houses
In JavaScript: You find XHR in older code and libraries
In real life: A smartphone is easier for most new calls
In JavaScript: fetch() is usually easier for new request code

In this lesson you will send real requests, but only to this website. The demos ask for one of these:

  • This page.
  • A page on this site that doesn't exist.
  • A blob: URL. A blob URL points at data that the page made itself, so no other server is involved.

One more word before we start. A header is a short line of extra information sent with a request or an answer, such as the type of data inside it.

The order to remember

open(method, url, async) gets the request ready. setRequestHeader() comes after open() and before send(). send(body) starts the request. Then events tell you what happened.

Three steps in order, joined by arrows. open() prepares only. setRequestHeader() adds a header. send() starts the trip.open()prepares onlysetRequestHeader()adds a headersend()starts the trip
Call the three methods in this order. Nothing leaves the browser until send() runs.

The XHR lifecycle

INTERACTIVE

A lifecycle is the list of steps something goes through, from start to end. A request has two sides:

  • The code you write.
  • The events the browser fires back. An event is a signal, such as "the request started" or "the answer arrived".

Here is the code side, in the order it runs:

Create, open, listen, then sendPop 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();
  1. Line 1 creates the request object.
  2. Line 2 calls open() with the method GET and the address in url.
  3. Line 3 sets a time limit, in milliseconds, with timeout.
  4. Lines 4 to 11 add one listener for each event. Every listener here is record, which prints the event's name.
  5. Line 12 calls send(), which starts the request.

The next example is a tiny fake request object. It shows the order of the calls without using the network. It uses status, the HTTP status code. That is a number the server sends back, such as 200 for "OK" or 404 for "Not Found".

A fake request object that records each callPop out in the code editor (opens in a new tab)JavaScript
const events = [];
const xhr = {
  status: 0,
  open(method, url) { events.push("open " + method + " " + url); },
  send() { this.status = 200; events.push("load " + this.status); },
};
xhr.open("GET", "/profile");
xhr.send();
console.log(events.join(" -> "));
  1. Line 4: open() saves the text open GET /profile.
  2. Line 5: send() sets status to 200 and saves load 200.
  3. Lines 7 and 8 call open(), then send().
  4. Line 9 prints open GET /profile -> load 200.

A 404 and a broken connection are not the same thing:

  • A 404 means the server answered. Its answer was "not found".
  • A broken connection means no answer came back at all.

XHR fires a different event for each. load means an answer arrived, whatever its status. error means no answer reached your code. This small model prints the two cases without contacting a server:

An answer with 404, or no answer at allPop out in the code editor (opens in a new tab)JavaScript
function describeOutcome(status, eventName) {
  if (eventName === "load") return "load with status " + status;
  return eventName + " with status " + status;
}

console.log(describeOutcome(404, "load"));
console.log(describeOutcome(0, "error"));

Line 6 prints load with status 404. Line 7 prints error with status 0.

Left: the server sends back a 404 answer, so the event is load and the status is 404. Right: no connection, so no answer comes back. The event is error and the status is 0.page not foundserver404 answerevent: loadstatus: 404no connectionserverno answerevent: errorstatus: 0
A 404 is still an answer from the server, so load fires. With no answer at all, error fires and status stays 0.

One more surprise: open() does not send anything. It only saves the method and the URL, and changes readyState to 1, which is named OPENED. The request leaves only when send() runs.

Send a real request and watch its events
The code this demo runsPop 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);
Events, in the order they fired

No request has run yet.

Try it yourself

Pick a target, then press Send. The request stays on this website.

Every target stays on this website: this page, a missing page on this site, or blob URLs that the page makes itself.

Try "Missing page". You get a real 404 status, but the outcome event is still load. That is because XHR answers two different questions:

  • Did an answer arrive? load says yes.
  • Did the server give you what you asked for? Only status tells you that.

Some requests get no answer that your code can read. In these cases status usually stays 0:

  • A revoked blob URL: the page has already thrown that data away.
  • A timeout: the time limit ran out.
  • An abort: your code stopped the request.
  • A CORS failure: another site didn't allow your page to read its answer.

readyState and events

STEP THROUGH

readyState is a number on the request object. It tells you which step the request has reached. It goes from 0 to 4. Each number also has a name, kept on XMLHttpRequest. For example, XMLHttpRequest.DONE is 4. People write the names in logs, because the numbers alone are hard to remember.

Real-life analogyreadyState is a pizza order

Ordering a pizza has steps you can name. You look at the menu. You call. The shop answers. The pizza is on its way. Then it arrives. An XHR request goes through five named steps in the same way.

In real life: 0: the menu is in your hand
In JavaScript: UNSENT: the object exists, but open() hasn't run
In real life: 1: you called the shop
In JavaScript: OPENED: the method and URL are set
In real life: 2: the shop answered the phone
In JavaScript: HEADERS_RECEIVED: the status and headers have arrived
In real life: 3: the pizza is on its way
In JavaScript: LOADING: the body of the answer is arriving
In real life: 4: the pizza arrives, even if it's the wrong one
In JavaScript: DONE: the request ended: it worked, failed, timed out, or was stopped

The answer from a server has two parts:

  • The headers: short lines of information, including the status.
  • The body: the main content, such as a page or some data.

In the table, each row is one step. Read it from left to right: the number, its name, what it means, and what you can read at that step.

The five readyState values
NumberNameWhat it meansWhat can you read now?
0UNSENTThe object exists, but it isn't opened yet.Nothing yet. You can only set the request up.
1OPENEDopen() has run. No answer has arrived yet.Nothing yet. You can only set the request up.
2HEADERS_RECEIVEDThe status and the headers have arrived.status and the response headers.
3LOADINGThe body is arriving.Progress, and sometimes the text received so far.
4DONEThe request has ended.status, the headers, and response.
Print each state number with its namePop out in the code editor (opens in a new tab)JavaScript
const names = ["UNSENT", "OPENED", "HEADERS_RECEIVED", "LOADING", "DONE"];
for (const state of [0, 1, 2, 3, 4]) {
  console.log(state + " " + names[state]);
}

The loop goes through the numbers 0 to 4. For each one, it prints the number and its name, from 0 UNSENT to 4 DONE.

Five cards in a row, joined by arrows. 0 UNSENT: new object. 1 OPENED: open() ran. 2 HEADERS_RECEIVED: status known. 3 LOADING: body arriving. 4 DONE: ended.0UNSENTnew object1OPENEDopen() ran2HEADERS_RECEIVEDstatus known3LOADINGbody arriving4DONEended
readyState counts up from 0 to 4. At 4, DONE, the request has ended, whether it worked or failed.

The events don't match the steps one to one. Here is the order they fire in:

  1. readystatechange fires each time readyState changes.
  2. loadstart fires once, when the request starts.
  3. progress fires while bytes of the answer arrive. A byte is one small piece of data.
  4. Exactly one outcome event fires: load, error, abort, or timeout.
  5. loadend fires last, every time. It is the place to clean up, such as hiding a loading spinner.

The readystatechange events happen in between the others, as the number goes up. Step through the cases below to see the full order.

Step through the events of one request
Step 0 of 12Ready
Your turn: follow the blue line

Step through the "Success (200)" case. Every case has the same shape: a start, then state changes (and progress, if bytes arrive), then one outcome event, then loadend.

Running in
  1. script
Next: line 1
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();
CallStoreChangeResultBlue line: runs next. 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.

Reading the answer: status and responseType

INTERACTIVE

When the answer arrives, XHR gives you these properties and methods to read it:

  • status is the HTTP status number, such as 200 or 404.
  • statusText is the short text for that status, such as "OK", when the browser has it.
  • responseURL is the final address of the answer. It can be different from the address you asked for, if the server sent you on to another address. That is called a redirect.
  • getAllResponseHeaders() returns the headers your code may see, as one string.

To read the body, use xhr.response. The kind of value it holds depends on responseType. You set responseType before you send:

  • "" (empty, the default) or "text": a string.
  • "json": a JavaScript value made from JSON text.
  • "blob": a Blob, which is raw data like a file.
  • "arraybuffer": an ArrayBuffer, which is raw bytes.
  • "document": an HTML or XML page, already turned into a Document object.

There is also an older property, responseText. It only works when responseType is "" or "text". For every other type, read response.

A JSON answer becomes a normal objectPop out in the code editor (opens in a new tab)JavaScript
const fakeResponse = { name: "Asha", items: 2 };
const responseType = "json";
console.log(responseType + ": " + fakeResponse.name);
console.log("items: " + fakeResponse.items);

This example doesn't send a request. Line 1 builds the object that a JSON answer would turn into. Line 3 prints json: Asha, and line 4 prints items: 2. With a real request, you would read the same fields from xhr.response.

With a real request, responseType = "json" asks the browser to turn the JSON text into a value before onload runs. If the text isn't valid JSON, response is null. So check for null before you read a property.

Change responseType and compare
Set responseType, then read responsePop 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();
What came back

Pick a type, then press "Request this page" to see what xhr.response holds.

Try it yourself

Pick a type, then press "Request this page" to see what xhr.response holds.

Each button asks for this same page. With json, response is null, because this page is HTML, not JSON. With document, the browser turns the HTML into a Document object.

In the table, each row is one property or method. The middle column says what it is for. The last column says what to look out for.

XHR answer features that are easy to mix up
Property or methodWhat is it for?What should you look out for?
responseTypeIt tells the browser what kind of value response should hold.Set it before the answer finishes arriving.
responseIt holds the body: a string, an object, a Blob, an ArrayBuffer, a Document, or null.With json, bad JSON gives null. It doesn't throw an error in onload.
responseTextIt holds the body as text.It only works for text. With other types, reading it can throw an error.
overrideMimeType()It tells XHR to read the answer as a different data type (MIME type), such as text/plain. Call it before send().It doesn't change what the server sent. Use it rarely.
withCredentialsIt sends cookies and login details with a request to another site.The other site must allow this in its CORS headers.

Upload progress: what XHR can do that fetch can't

INTERACTIVE

Uploading means sending data from your browser to a server, such as a photo. Download progress tells you that bytes are arriving. Upload progress tells you that bytes are leaving. XHR has an object just for upload events: xhr.upload.

Real-life analogyUpload progress is a moving truck

You are moving house. Boxes go onto a truck one by one. Someone ticks each box off on a clipboard, so you always know how many boxes are on the truck.

In real life: Boxes go onto the truck one by one
In JavaScript: Bytes leave the browser
In real life: A clipboard counts the boxes loaded so far
In JavaScript: xhr.upload.onprogress reports the bytes sent so far
In real life: The house at the other end can still refuse the delivery
In JavaScript: The server can still answer 404 or 405 after the upload events

The demo below sends a made-up text Blob to this page with POST. POST is the HTTP method for sending data. This website doesn't accept uploads, so the server may answer with an error after the upload. That is fine: we only want to watch the upload events.

Watch an upload's progress
Count the bytes as they leavePop 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. Press the button to send a made-up Blob to this page.
Try it yourself

Press the button to send a made-up Blob to this page.

XHR has a separate xhr.upload object for upload events. fetch() has no standard upload progress events, even though it can read an answer piece by piece.

fetch() can show download progress, by reading the answer piece by piece. But it has no standard upload progress events like xhr.upload.onprogress. That is the main reason many upload bars still use XHR.

A progress event has three properties:

  • loaded: the bytes sent so far.
  • total: the full size in bytes, if it is known.
  • lengthComputable: true when the full size is known, and false when it isn't.
Show a percent only when the total is knownPop out in the code editor (opens in a new tab)JavaScript
function progressMessage(event) {
  if (!event.lengthComputable) return event.loaded + " bytes uploaded";
  return Math.round((event.loaded / event.total) * 100) + "% uploaded";
}

console.log(progressMessage({ loaded: 240, total: 400, lengthComputable: true }));
console.log(progressMessage({ loaded: 240, total: 0, lengthComputable: false }));
  1. Line 2: if the total isn't known, return the bytes sent so far.
  2. Line 3: otherwise, work out the percent and round it.
  3. Line 6 passes 240 of 400 bytes with the total known, so it prints 60% uploaded.
  4. Line 7 passes 240 bytes with no known total, so it prints 240 bytes uploaded.
Top row: lengthComputable is true. loaded 240, total 400 becomes 60% uploaded. Bottom row: lengthComputable is false. loaded 240, total 0 becomes 240 bytes uploaded.lengthComputable: trueloaded 240, total 40060% uploadedlengthComputable: falseloaded 240, total 0240 bytes uploaded
When the total size is known, you can show a percent. When it isn't, show the bytes sent so far.

Only work out a percent when lengthComputable is true. If you divide by an unknown total, you get a number that looks exact but is wrong. Show the bytes sent instead, or a bar that moves without a percent.

Wrapping XHR in a promise

PRACTICAL

XHR is older than promises. A promise is an object that will later hold a result or an error. You read it with then, catch, or await.

You can put an XHR request inside a promise. Then the rest of your app uses await, while the inside still listens to XHR events. You saw this idea in the Promisification & promise-based APIs lesson.

Put an XHR request inside a promisePop 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);  });}
  1. Line 2 makes a new promise. Calling resolve gives it a result. Calling reject gives it an error.
  2. Lines 3 to 5 create the request, open it, and set its responseType.
  3. Line 6: when load fires, the promise gets the status and the response.
  4. Lines 7 to 9: when error, timeout, or abort fires, the promise gets an error.
  5. Lines 10 to 12 add each header from options.headers.
  6. Line 13 sends the request, with a body if there is one.

Look at what this wrapper does with a 404. It still calls resolve, because a 404 fires load. It only calls reject when no answer came: a network error, a timeout, or an abort. So your code still has to check status itself.

Check the status yourselfPop out in the code editor (opens in a new tab)JavaScript
function isHttpSuccess(status) {
  return status >= 200 && status < 300;
}

console.log(isHttpSuccess(204));
console.log(isHttpSuccess(404));

A status from 200 to 299 means success. Line 5 prints true for 204. Line 6 prints false for 404.

So a 404 resolves the wrapper's promise, but fails this check. A network error rejects the promise, and there is no status to check.

All setup goes after open() and before send():

Set everything up between open() and send()Pop 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" }));
  1. Line 2 opens a POST request to /api/messages.
  2. Line 3 asks for the answer as JSON.
  3. Line 4 sets a time limit of 5000 milliseconds, which is 5 seconds.
  4. Line 5 sends cookies and login details with the request, with withCredentials.
  5. Line 6 adds a header that says the body is JSON.
  6. Line 7 sends the body, a JSON string.
Don't use synchronous XHR

open() has a third argument, async. If you pass false, the request is synchronous: the page stops and waits until the answer arrives. Clicks don't work, and nothing on the screen changes. Browsers have deprecated this on the main page, which means it is old and may be removed. If you see it in old code, change it to the normal way, with async left as true.

When XHR is still useful

SORT

New request code usually starts with fetch(). Here is why:

  • It returns a promise, so it works with await.
  • It can be cancelled with an AbortSignal, an object that tells a request to stop.
  • It can read an answer piece by piece. This is called a stream.

XHR is still useful in three places:

  • Upload progress: use xhr.upload.onprogress for file upload bars.
  • Older code: old jQuery request code and older libraries often use XHR inside.
  • Finding bugs step by step: the ready states and events show each step of a request, so you can see where it went wrong.
XHR or fetch?
  • Show a progress bar while a file uploads
  • Read an answer piece by piece as it arrives
  • Handle a request inside a service worker
  • Keep old jQuery request code working
  • Cancel with AbortSignal
  • Load JSON from your own site and read it
  • Send a request with your own headers
Try it yourself
0 of 7 correct

Put each job under the tool you would pick 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, but not gone. Browsers still support it, so that old pages keep working.
  • "A 404 fires error." No. A 404 is still an answer, so load fires. Check status to see if it worked.
  • "Status 0 is a real HTTP status." No. It means no answer your code can read. Common causes are an abort, a timeout, a network failure, or CORS.
  • "Progress always knows the total." No. lengthComputable can be false. Then show the bytes sent, without a percent.
  • "responseText always works." No. It only works for text. For other response types, use response.
  • "Synchronous XHR is a handy shortcut." No. It freezes the page, and browsers have deprecated it on the main page. Don't 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 the status that this short program prints.

    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: a header set too early

      This code has the right lines in the wrong order. Fix the order.

      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 clean-up handler

        A spinner is a turning icon that means "loading". Write the handler you would add before send(). It should hide the spinner, however the request ends.

        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 · ChallengeWrite an upload progress message

          Write a progress message that still works when the browser doesn't 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 7In what order do you call the two main XHR methods?

              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 answers an XHR request with a 404. Which outcome event should your code 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 do you use 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 browsers still support it, and older code uses it a lot.
            • The order is open(), then any setup and headers, then send().
            • readyState goes from 0 to 4. loadend is always the last event, so clean up there.
            • A 404 or 500 answer still fires load. A network failure, an abort, or a timeout fires its own event, and status usually stays 0.
            • Use xhr.upload.onprogress when you need real upload progress.

            Final definition: XMLHttpRequest is the browser's older object for sending HTTP requests. It tells you what happens through events. It's worth knowing for its ready states, its progress events, and the older code that still uses it.

            Up next: Cookies.

            CompleteFrontend Clear concepts. Working examples.