A while back, I was working with a small JavaScript library responsible for sending a POST request with some data to an endpoint. At the time, it used axios to make that request, and I wanted to simplify things by shedding a dependency. The obvious alternative was fetch
— modern, native, and ergonomic.
But in this case, the following bits of context made me wonder if the obvious choice was the best choice:
- The package would be distributed amongst several teams.
- The package had a simple, single responsibility.
- The package needed to work for users on IE11.
Where Fetch Held Me Up
The fetch
API is a welcomed upgrade to making HTTP requests in JavaScript, but in order to leverage it here, I’d need to rely on two different polyfills: the Promise object, and the fetch
API itself. And that would mean putting more of a burden on the teams who implement it, as well as the users who interact with it:
- It’d require teams to set up additional dependencies, which would involve vetting which polyfills to use (there are several for any given API), ensuring none are already being loaded by the application, and potentially working through unforseen issues.
- Unless some sort of differential serving is set up, it’d require most users to download polyfills they don’t actually need (~94%+ are on browsers that support
fetch
).
For my simple needs, this just felt like too much.
Making Prehistoric HTTP Requests
So, I thought back to what our ancestors used to do such things: XMLHttpRequest
. The O.G. of HTTP requests in JavaScript. I’ve heard rumors of this thing. The verbosity. The insanity it’s left in its wake.
Despite that reputation, I gave it a shot wiring it up. And as it turned out, for simple requests, most of those rumors were overblown. After the switch, my implementation went from something like this:
try {
let response = await axios.post('http://localhost:4000', {
name: 'Alex'
}, {
headers: {
'x-api-key': 'my-api-key'
}
});
console.log(response.data);
} catch (e) {
console.log('Request failed!');
}
To something more like this:
const xhr = new XMLHttpRequest();
xhr.open('POST', "http://localhost:4000");
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('x-api-key', 'my-api-key');
xhr.onload = function () {
if (this.status >= 200 && this.status < 400) {
console.log(JSON.parse(this.responseText));
return;
}
console.log('Something went wrong!');
};
xhr.onerror = function () {
console.log('Something went wrong!');
}
xhr.send(JSON.stringify({ name: 'Alex' }));
That’s a very similar amount of code for virtually the same functionality. And no polyfills.
Why XMLHttpRequest Made Sense
Given all that aforementioned context, a few key perks surfaced as a result of switching to XMLHttpRequest
.
1. Less code shipped.
Being so old-school in terms of making HTTP requests, browser support isn’t even remotely a concern. By using it, I can avoid loading those any polyfills still required to use fetch
in IE, saving me about ~4kb of bundled code (assuming I would’ve used these two pretty good polyfills I came across):
Polyfill | Size (minified) | Size (minified + gzipped) |
---|---|---|
promise-polyfill |
2.9kb | 1.1kb |
unfetch |
1kb | 554b |
Those savings aren’t monumental, but they shouldn’t be scoffed at either, especially considering the low amount of effort on my part, and the fact that those savings will be multipled throughout several different projects.
2. Simpler distribution.
Being polyfill-free, I don’t need to worry about asking other teams to deal with extra dependencies at all. No vetting process, no added documentation. Just grab the library & go. This also means we’ll be avoiding the consequences that arise when teams inevitably fail to read that added documentation.
3. Less risky implementation.
When pulling in the package, teams don’t need to deal with the array of potential issues that come up from introducing global dependencies, such as double-loading polyfills that are already being loaded, or subtle differences in how a polyfill behaves relative to the actual specification. Any risk in implementing the library is limited to the package code itself. In general, the JavaScript polyfill landscape is the wild west, with no guarantees that packages will meet the full specification of an API (in fact, many don’t intend to). Being able to sidestep the unavoidable risks in dealing with them is huge.
Some Common Objections
Despite these good things, there are a few objections I’ve seen come up a few times:
1. We should lean into writing modern JavaScript!
Agreed, but not if that means doing so for the sake of writing modern JavaScript. If “modern” code introduces complexity and costs that could’ve otherwise been avoided, and if the alternative isn’t that much work, there’s no shame in going old-school. There’s a balance that needs to be found with every project, and more often than not, the “new” might have the best case. But more classic solutions shouldn’t be immediately dismissed exclusively because there’s a flashier (or just easier) option out there.
2. Isn’t XMLHttpRequest deprecated?
No. A part of it (the ability to make synchronous HTTP requests) is in the process of being removed from the platform due to the horrid performance issues that come along with it. But the core API itself isn’t going anywhere, and still offers advantages over fetch
, like being able to track progress on file uploads.
By using XMLHttpRequest
, you’re not just piling on tech debt you’ll need to clean up a couple years from now. In fact, choosing it might actually set you up for less work in the future, since you’d otherwise be removing polyfills when they’re no longer needed (assuming you currently need to support IE).
3. That API is disgusting!
Yeah, it is. That’s why I’m placing decent emphasis on it being best for simple requests. The instant the scope of a package goes beyond that, or as soon as you drop IE as a supported browser, fetch
(or something else) might be a better way to go. Until then, at the very least, play with it for a while instead of dismissing it based off water cooler dev chatter. You’ll likely discover (like I did) that it’s not nearly as bad as people make it out to be.
4. I like my Promise-based API!
Me too! But thankfully, it’s easy enough to wrap an XMLHttpRequest
implementation in a Promise to retain that interface. You’ll get those ergonomics, and you’ll still have to deal with one less polyfill than if you had gone with something like fetch
.
const fire = () => {
const xhr = new XMLHttpRequest();
xhr.open('POST', "http://localhost:4000");
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('x-api-key', 'my-api-key');
return new Promise((resolve, reject) => {
xhr.onload = function () {
if (this.status >= 200 && this.status < 400) {
return resolve(JSON.parse(this.responseText));
} else {
return reject(new Error('Something went wrong!'));
}
};
xhr.onerror = function () {
return reject(new Error('Something went wrong!'));
}
xhr.send(JSON.stringify({ name: 'Alex' }));
});
}
(async () => {
try {
console.log(await fire());
} catch(e) {
console.log(e.message);
}
})();
Sometimes, New Might Not Be Best
It’s easy to get excited about advancements to web APIs like fetch
. But if we’re not careful, it’s just as easy to become dogmatic about using newer technologies exclusively because they’re new. As you wade these waters, try to keep the full scope of your circumstances in mind — the users, the needs, the environment, everything. You may find out that the best tool for the job is the one that’s been around since your grandma was making HTTP requests.
(This is an article published at macarthur.me. Read it online here.)
Top comments (1)
I've always used XHR. I actually didn't know about fetch until a few months ago. Usually I just have a couple wrapper functions so that I can quickly GET and POST and not think too much about building an XHR object.
It's nice to see that my choice to just keep using it over the cool stuff has some merit!