Let’s talk about two staples of HTTP requests in JavaScript: the fetch() API and XMLHttpRequest (XHR). You’re probably using fetch() daily—it’s the modern standard, after all—but XHR? It’s more like that vintage vinyl player in the corner: not spinning much these days, but it’s got a story worth knowing. Understanding fetch versus XHR isn’t about picking a winner (spoiler: fetch already won); it’s about appreciating where we’ve been and why fetch rules the roost in 2025. So, let’s break it down—history, mechanics, and all—with some code to keep it real.
The Backstory: XHR’s Glory Days and Fetch’s Rise
Picture this: it’s the late ‘90s, and the web is mostly static pages and dial-up screeches. Microsoft drops XMLHttpRequest into IE5 as an ActiveX oddity. Fast forward to the mid-2000s, and XHR becomes the backbone of AJAX—those slick, no-refresh updates powering Gmail and Google Maps. It was a game-changer, letting devs snag data asynchronously with a clunky but effective API. Every browser adopted it, and XHR reigned supreme for years.
Then, in 2015, fetch() arrived, cooked up by the WHATWG crew and rolled out in modern browsers like Chrome and Firefox. It wasn’t just a new toy—it was a rethink. Promise-based, streamlined, and free of XHR’s baggage, fetch() was built for a JavaScript world that had outgrown callbacks. By 2025, XHR’s a relic; fetch() is baked into Node.js (since v18) and powers everything from SPAs to APIs. But how’d we get from there to here? Let’s compare.
How They Work: Fetch vs XHR Under the Hood
Both fetch() and XHR fetch data from servers, but their approaches? Night and day. Here’s the breakdown with examples to show the shift.
1. Syntax: Old School vs New School
XHR is verbose and event-driven—think manual gears. Fetch is Promise-based and sleek. Here’s a simple GET request:
XHR: The Classic Way
javascriptconst xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/users', true);
xhr.onload = function () {
// Yes, this was a best practice back in the day.
if (xhr.status >= 200 && xhr.status < 300) {
const data = JSON.parse(xhr.responseText);
console.log('Users:', data);
} else {
console.error('Error:', xhr.status);
}
};
xhr.onerror = function () {
console.error('Network issue!');
};
xhr.send();
Fetch: The Modern Way
javascript// Defaults to GET method
fetch('https://api.example.com/users')
.then(response => {
if (!response.ok) throw new Error(`Status: ${response.status}`);
return response.json();
})
.then(data => console.log('Users:', data))
.catch(error => console.error('Oops:', error));
Or, with async/await:
javascriptasync function getUsers() {
try {
const response = await fetch('https://api.example.com/users');
if (!response.ok) throw new Error('Something’s off!');
const data = await response.json();
console.log('Users:', data);
} catch (error) {
console.error('Oops:', error);
}
}
getUsers();
XHR’s a slog—setting up events, parsing JSON manually. Fetch hands you a clean chain and built-in methods like response.json()
. It’s no wonder XHR’s faded out.
2. Error Handling
XHR flags errors via status codes in onload
—you sort it out. Fetch only rejects Promises on network failures; HTTP errors like 404 or 500 resolve as “successful” unless you check response.ok
.
XHR: Status Check
javascriptxhr.onload = function () {
if (xhr.status === 404) {
console.error('Not found!');
} else if (xhr.status === 200) {
console.log(JSON.parse(xhr.responseText));
}
};
Fetch: Watch That ok
javascriptfetch('https://api.example.com/missing')
.then(response => {
if (!response.ok) throw new Error('Not found!');
return response.json();
})
.catch(error => console.error(error));
3. Features: What They Bring to the Table
XHR had some neat tricks—like onprogress
for tracking uploads:
javascriptxhr.upload.onprogress = event => {
const percent = Math.round((event.loaded / event.total) * 100);
console.log(`Uploaded ${percent}%`);
};
Fetch doesn’t do this natively (streams can approximate it), but it counters with modern wins: readable streams for big data and AbortController
for canceling requests:
Aborting a Request
javascriptconst controller = new AbortController();
fetch('https://api.example.com/big-data', { signal: controller.signal })
.then(response => response.json())
.catch(err => console.error(err));
setTimeout(() => controller.abort(), 5000); // Bail after 5s
Progress bar with fetch
javascriptasync function downloadWithProgress(url) {
const response = await fetch(url);
const contentLength = response.headers.get('content-length');
const total = parseInt(contentLength) || 0;
let loaded = 0;
const reader = response.body.getReader();
const chunks = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
loaded += value.length;
const progress = total ? (loaded / total) * 100 : 'unknown';
console.log(`Downloaded ${progress.toFixed(2)}%`);
const progressBar = document.querySelector('#progress');
if (progressBar) {
progressBar.style.width = `${progress}%`;
progressBar.textContent = `${progress.toFixed(1)}%`;
}
}
const blob = new Blob(chunks);
return blob;
}
downloadWithProgress('https://api.example.com/large-file')
.then(blob => console.log('Download complete:', blob))
.catch(error => console.error('Download failed:', error));
4. POST Requests: Sending Stuff
Both can POST, but fetch simplifies it:
XHR POST
javascriptxhr.open('POST', 'https://api.example.com/users', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify({ name: 'Furkan' }));
Fetch POST
javascriptfetch('https://api.example.com/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Furkan' })
}) .then(response =>
response.json()
);
Fetch’s options object is just more ergonomic.
Browser Support: Where They Stand in 2025
Today, fetch() is everywhere: Chrome, Firefox, Safari, Edge, Node.js. It’s the default, no questions asked. XHR? It’s universal too, even in ancient IE6, nobody’s running IE11 unless they’re stuck in a corporate time capsule. Fetch’s dominance is complete, though older codebases might still lean on XHR for historical reasons.
Why Fetch Took Over
Fetch didn’t just edge out XHR—it lapped it. Promises and async/await fit today’s JavaScript like a glove, and fetch’s streamlined API cuts the cruft. It’s built for streams, service workers, and a web that’s all about real-time and scale. XHR’s still got niche uses (progress tracking, anyone?), but it’s a museum piece in 2025—respected, not used.
A Fetch Polyfill: Bridging the Past
For fun, here’s a simple fetch polyfill using XHR—think of it as a history lesson in code. It’s not for daily use (fetch is native now), but it shows how to mimic fetch in a pinch:
javascriptif (!window.fetch) {
window.fetch = function (url, options) {
if (!options) {
options = {};
}
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
const method = (options.method || 'GET').toUpperCase();
xhr.open(method, url, true);
if (options.headers) {
for (const [key, value] of Object.entries(options.headers)) {
xhr.setRequestHeader(key, value);
}
}
xhr.responseType = options.responseType || 'text';
xhr.onload = function () {
const response = {
ok: xhr.status >= 200 && xhr.status < 300,
status: xhr.status,
statusText: xhr.statusText,
json: () => Promise.resolve(
xhr.responseType === 'json' ? xhr.response : JSON.parse(xhr.responseText || xhr.response)
),
text: () => Promise.resolve(
xhr.responseType === 'text' ? xhr.response : String(xhr.response)
),
blob: () => Promise.resolve(
xhr.responseType === 'blob' ? xhr.response : new Blob([xhr.responseText || xhr.response])
)
};
if (response.ok) {
resolve(response);
} else {
reject(new Error(`Status: ${xhr.status}`));
}
};
xhr.onerror = () => reject(new Error('Network error'));
xhr.send(method === 'GET' || method === 'HEAD' ? null : options.body);
});
};
}
// Test it
fetch('https://api.example.com/users')
.then(res => res.json())
.then(data => console.log(data))
.catch(err => console.error(err));
Step-by-Step:
-
Check Support: If
fetch
isn’t there, define it. - Options: If no options are provided, create an empty object.
- Promise It Up: Wrap XHR in a Promise to match fetch’s style.
-
Map Options: Translate fetch’s
method
,headers
, andbody
to XHR. -
Fake a Response: Build a fetch-like object with
ok
,json()
, etc. - Handle Errors: Reject on network issues or bad statuses.
It’s barebones—no streams or aborting—but it mimics fetch’s core. Real-world polyfills like whatwg-fetch
do more, but this shows the bridge from XHR to fetch.
Final Thoughts
Fetch vs XHR isn’t a debate anymore—fetch is the present and future. Still, knowing XHR’s quirks and legacy gives you context for why fetch feels so good. It’s like appreciating black-and-white TV before streaming 4K. Next time you’re chaining a .then()
or awaiting a response, give a nod to XHR—it got us here.
Album of the day: