XMLHttpRequest vs fetch
By FrontendPro Editorial Team Updated 9/26/2026
Answer
Both make HTTP requests from JavaScript. fetch is the modern replacement; XHR survives in legacy code and in two niches it still owns.
// XHR — callbacks, mutable object
const xhr = new XMLHttpRequest();
xhr.open("GET", "/api/data");
xhr.onload = () => { if (xhr.status === 200) console.log(JSON.parse(xhr.responseText)); };
xhr.onerror = handleError;
xhr.send();
// fetch — promises
const res = await fetch("/api/data");
if (!res.ok) throw new Error(res.status);
const data = await res.json();
| XHR | fetch | |
|---|---|---|
| Style | Events and callbacks | Promises, works with await |
| HTTP errors | Check status | Check res.ok — does not reject |
| Cancellation | xhr.abort() | AbortController |
| Upload progress | onprogress | Not supported |
| Download progress | onprogress | Via a stream reader |
| Streaming the body | No | Yes, res.body is a stream |
| Cookies cross-origin | Sent by default | Need credentials: "include" |
| Timeout | xhr.timeout | AbortSignal.timeout() |
The two surprises
fetch does not reject on 404 or 500 — only on network failure. Forgetting res.ok is the single most common bug with it.
And cross-origin cookies are not sent by default, unlike XHR. A request that worked with XHR can silently become unauthenticated after migrating.
Where XHR is still the answer
Upload progress. There is no fetch equivalent, so file upload with a progress bar still uses XHR, or a library that wraps it. Older synchronous XHR exists but blocks the main thread and should never be used.
In practice
Write fetch for everything new, ideally behind a small wrapper that checks res.ok and handles JSON, so you do not repeat those lines. Reach for XHR only for upload progress, and recognise it in code you inherit.
