AJAX and XMLHttpRequest
Before fetch there was AJAX — Asynchronous JavaScript And XML — and the API at the heart of it was XMLHttpRequest (XHR). It is the original way browsers made HTTP calls without reloading the page. Today fetch is the default for new code, but XHR has not gone away: it lives on inside libraries, in legacy applications, and as the only standard browser API for upload progress events.
What XHR is
XMLHttpRequest is a constructor that creates a request object. You configure it (method, URL, headers), attach event listeners for state changes, then call .send(). The transfer happens asynchronously and the events fire as the response arrives.
Introduced by Microsoft in 1999 (in Outlook Web Access) and standardised across browsers a few years later.
The "XML" in the name is historical — the response can be plain text, JSON, a
Blob, anArrayBufferor aDocument.It powered the AJAX revolution that made Gmail, Google Maps and modern single-page apps possible.
A basic GET with XHR
const xhr = new XMLHttpRequest();
xhr.open("GET", "/api/user/42");
xhr.responseType = "json"; // automatic parsing
xhr.addEventListener("load", () => {
if (xhr.status >= 200 && xhr.status < 300) {
console.log("user:", xhr.response);
} else {
console.error("server error", xhr.status);
}
});
xhr.addEventListener("error", () => console.error("network error"));
xhr.send();Compare that to the same operation with fetch:
The fetch equivalent
const response = await fetch("/api/user/42");
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const user = await response.json();POSTing JSON
function postJson(url, payload) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("POST", url);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.responseType = "json";
xhr.addEventListener("load", () => {
if (xhr.status >= 200 && xhr.status < 300) resolve(xhr.response);
else reject(new Error(`HTTP ${xhr.status}`));
});
xhr.addEventListener("error", () => reject(new Error("network error")));
xhr.addEventListener("abort", () => reject(new Error("aborted")));
xhr.send(JSON.stringify(payload));
});
}Most XHR usage in modern code looks like this — wrap it once in a promise and never touch the constructor again.
Events and readyState
Each XHR moves through five readyState values; the readystatechange event fires at every transition. In day-to-day code you can ignore readyState and listen to the named events:
loadstart— the request has been sent.progress— fires repeatedly while the response is downloading. Useful for progress bars.load— the response is complete (success or HTTP error).error— a network-level failure (DNS, CORS, offline).timeout—xhr.timeoutwas exceeded.abort—xhr.abort()was called.loadend— fires after any of the above. The "finally" event.
Upload progress — XHR's last superpower
fetch does not expose upload progress at the spec level (request streams are coming, but uneven). XHR has had it since the beginning via xhr.upload:
const xhr = new XMLHttpRequest();
xhr.open("POST", "/upload");
xhr.upload.addEventListener("progress", (event) => {
if (event.lengthComputable) {
const pct = Math.round((event.loaded / event.total) * 100);
progressBar.value = pct;
}
});
xhr.addEventListener("load", () => console.log("done"));
xhr.send(formData);This is the single most common reason you still see XHR in 2026 — file upload widgets in apps that want a progress bar.
Cancellation, timeout, credentials
xhr.abort()cancels in-flight requests — the fetch equivalent ofcontroller.abort().xhr.timeout = 5000rejects with atimeoutevent after 5 seconds — fetch needsAbortSignal.timeout.xhr.withCredentials = truesends cookies cross-origin — likefetch({ credentials: "include" }).xhr.setRequestHeader(name, value)adds a header; some headers (e.g.Host,Cookie) are forbidden by the spec.
When you still need to know XHR
You are debugging a legacy codebase or an older library (jQuery
$.ajax, Axios in some configurations) that wraps XHR.You need granular upload progress — XHR is still the most portable answer.
You are dealing with a quirky environment (some service workers, older browsers, embedded webviews) where
fetchis missing or behaves oddly.You are reading older blog posts or interview material that assumes XHR knowledge.