, ,

5 Key Differences Between XMLHttpRequest and Fetch in JavaScript

Posted by

Both send HTTP requests, but only one feels modern. Here’s how Fetch simplifies everything XHR made complicated.

Both send HTTP requests, but only one feels modern. Here’s how Fetch simplifies everything XHR made complicated.

Introduction

Before fetch() arrived, XMLHttpRequest (XHR) was our only way to make HTTP calls in JavaScript.

It worked… but it was messy:

  • Verbose syntax
  • Hard-to-read callbacks
  • Confusing error handling
  • No native Promise support

Then came Fetch, and everything changed.

The Fetch API isn’t just a newer way to do XHR it’s a cleaner, promise-based model that aligns with modern async JavaScript.

Let’s break down the 5 key differences between the two, with real code examples and practical takeaways.


1️. Promises vs Callbacks

🧠 The Concept

The biggest difference: Fetch is Promise-based, while XHR is callback-based.

Promises make asynchronous code cleaner, chainable, and easier to debug.

❌ XMLHttpRequest

const xhr = new XMLHttpRequest();
xhr.open("GET", "/api/data");
xhr.onload = function () {
if (xhr.status === 200) {
console.log("Data:", JSON.parse(xhr.responseText));
} else {
console.error("Error:", xhr.status);
}
};
xhr.onerror = function () {
console.error("Network error");
};
xhr.send();

Messy, right?

You have to track onload, onerror, readyState, and status manually.

✅ Fetch

fetch("/api/data")
.then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then(console.log)
.catch(console.error);

Cleaner, declarative, and it works perfectly with async/await:

try {
const res = await fetch("/api/data");
const data = await res.json();
console.log(data);
} catch (err) {
console.error(err);
}

💡 Takeaway:

Fetch simplifies async flow no onreadystatechange headaches, no callback pyramids.


2️. Response Handling

❌ XHR

You access the response via properties like responseText, status, and readyState.

xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
const data = JSON.parse(xhr.responseText);
console.log(data);
}
};

It’s manual, stateful, and error-prone.

✅ Fetch

Fetch introduces the Response object, which is far more powerful:

const res = await fetch("/api/users");
console.log(res.status); // 200
console.log(res.ok); // true
const data = await res.json();

You can also access:

  • .text() → for plain text
  • .blob() → for files
  • .arrayBuffer() → for binary data
  • .headers → for response headers

💡 Takeaway:

Fetch’s response object gives you fine-grained control with a simple, consistent API.


3️. Error Handling

❌ XHR

Network errors, timeouts, and HTTP errors are handled separately:

xhr.onerror = () => console.error("Network error");
xhr.ontimeout = () => console.error("Request timed out");

And remember, XHR doesn’t throw errors on bad status codes. You must handle them manually.

✅ Fetch

Fetch throws only on network errors, not on HTTP errors (like 404 or 500).
 But you can easily check .ok:

const res = await fetch("/api/data");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();

Plus, it integrates naturally with try/catch, AbortController, and Promise.all.

💡 Takeaway:

Fetch streamlines error handling fewer event handlers, more predictable control flow.


4️. Request Customization and Headers

❌ XHR

Setting headers and sending data feels verbose:

const xhr = new XMLHttpRequest();
xhr.open("POST", "/api/create");
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send(JSON.stringify({ name: "Rahmat" }));

And if you forgetContent-Type, your backend won’t parse the data.

✅ Fetch

Fetch lets you customize everything cleanly:

await fetch("/api/create", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "Rahmat" }),
});

Need authorization? Easy.

await fetch("/api/user", {
headers: { Authorization: `Bearer ${token}` },
});

💡 Takeaway:

Fetch treats configuration as data, not as procedural steps.
You build your request with options, not boilerplate.


5️. Stream Handling and Modern Features

Fetch isn’t just cleaner, it’s more powerful.

🔥 Modern capabilities

Fetch supports features XHR simply doesn’t:

Example: Streaming

const res = await fetch("/large-file");
const reader = res.body.getReader();

let total = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
total += value.length;
console.log("Received bytes:", total);
}

You could never do this easily with XHR.

💡 Takeaway:

Fetch isn’t just modern it’s designed for scalability, performance, and streaming-first APIs.


Bonus: Simpler CORS and JSON Handling

Fetch handles CORS and JSON parsing more gracefully.

fetch("/api/data", { mode: "cors" })
.then((r) => r.json())
.catch(console.error);

In XHR, CORS requires extra flags and a complex setup.

✅ Fetch defaults to modern web security and plays well with frameworks like React, Next.js, and Deno.


Summary Table XHR vs Fetch


Why Fetch Wins

Fetch isn’t just syntactic sugar; it represents the modern JavaScript mindset:
 ✅ Functional composition
 ✅ Promise-based async flow
 ✅ Readable, predictable API

Once you understand Fetch, you’ll never want to touch XMLHttpRequest again.

And since it’s now supported natively across browsers and Node.js, there’s no reason not to go all-in.


Conclusion

Both XHR and Fetch send HTTP requests, but Fetch redefines how you think about network calls.

In summary:

  1. Fetch uses Promises instead of callbacks.
  2. It offers a cleaner response model.
  3. Error handling is simpler and more consistent.
  4. Headers and options are declarative.
  5. It supports streaming, async/await, and AbortController.

Fetch isn’t just newer it’s better in every way that matters.


Call to Action

Have you migrated your old XMLHttpRequest code yet?
 Share your before-and-after examples in the comments 👇

And if your teammate still uses it xhr.open() in 2025… send them this post. It’s time for an upgrade.

Leave a Reply

Your email address will not be published. Required fields are marked *