How to get data with JavaScript in 2018

There are a number of different ways to contact a server and ask it kindly for data. Let’s have a look at the current standards.

Matt Burgess

--

No matter what technology or framework you use for your frontend app there near invariably comes a point where you have to ask a grown-up language for help. Maybe this is a REST api, maybe just a rough collection of data on an endpoint, it doesn’t really matter — an XHR request must be made.

There currently exists a number of technologies to make this happen, and they all have strengths and weaknesses. Let’s take a look at them one by one.

Browser Built In

These are things the browser supports natively. Or should. Or will. In all cases here modern browsers will have native support, but older browsers (rhymes with Blinternet Blexplorer) might need a polyfill.

Straight vanilla XHR

Do you ever think to yourself “Gosh, I hate myself and would like to make my job as hard as possible”? If so, native XHR is for you.

This is the underlying technology behind the pattern we know as “ajax”, XHR -short for XML HTTP Request. Ajax, just by the way stands for Asynchronous JavaScript and XML if it’s not considered to be an acronym (actual word based on letters) on its own merits.

var xmlhttp = new XMLHttpRequest();
xmlhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
displayProjects(JSON.parse(this.responseText));
}
};
xmlhttp.open('GET', 'api/projects');
xmlhttp.send();

Intuitive, especially the onreadystatechange part. Remember, the readyState has to be 4. Obviously.

Making a POST is similarly inelegant.

var formData = new FormData(document.getElementById('my-form'));
var xmlhttp = new XMLHttpRequest();
xmlhttp.open("POST", 'api/projects');
xmlhttp.send(formData);

Note that these are very simple cases. There’s no error handling, the form is sent entirely as-is, they are synchronous, and they perform no action on completion.

XHR is supported natively on any browser made after the industrial revolution.

Promises

Promises are a bit of a mislead here. They don’t actually do any requesting on their own. A promise is instead a wrapper around functionality that lets it be used asynchronously, and trigger behaviour after it’s completed. This is done by calling .then() when the promise resolves. You can then call .then() on that, and so on, chaining behaviour.

In actual practise, you don’t have to use native Promises alone. They’re critical to understand because other http clients use and return them. This includes things like jQuery's$.ajax.

In production use, Promises are kind of a pain in the ass. Functions you expect to return data actually return a promise, and nested promises can turn into some ugly chains of “then”. The result is a fairly unintuitive pattern of asynchrony occurring in code that arrows to the right, rather than executing down. Promises are also routinely implemented with little or no error handling, so errors simply… vanish. This can be fixed with a .catch block as well as a .then, but that takes whole seconds to write, and you’re definitely not going to need it.

Native Promises should be supported on every non-garbage browser. IE support will require a library like RSVP or Q.

Fetch

The fetch syntax is the second coming of Christ for web developers. It provides an API that is identical for both client-side applications and server-side (such as NodeJS) and that facilitates SSR. It also facilitates use by service workers. Fetch provides a clean, elegant, and simple API that eliminates the need for complex http libraries. Yay!

Pity it’s shit.

Sorry, but it is. Ok, let’s take a look at some code.

fetch('api/projects').then(response => {
let data = response.json();
});

Boom. That’s actually pretty simple, hey. Not so bad! Except it’s wrong. Actually, the response.json() doesn’t return json, it returns a “readable stream” which is a promise which needs to be resolved to get the actual data. So you really end up with the following, and start to see the “nesting arrow to the right” that I mentioned in the promises section.

fetch('api/projects').then(response => {
response.json().then(json => {
let data = json;
});
});

Heaven help us if we need to use data.id to make another fetch request or something.

POST requests are way worse. The documentation will tell you to do it like this. (Assume for the sake of discussion that formData is an object created elsewhere.)

fetch("api/projects", {
method: "POST",
body: formData
});

Naturally the documentation is a web of lies and pain. In fact, the body will come through as invalid or empty. Your backend will just get sent some sort of null request. This is because the object sent in body specifically needs to be run through JSON.stringify before it’s valid to be sent.

The other issue is that because you’re posting JSON, you will need to explicitly set headers or it may well not work at the other end.

fetch("api/projects", {
method: "POST",
body: formData,
headers: {
"Content-Type": "application/json"
}
});

I’m not 100% sure of the accuracy of this for all setups. I just know that there were some hoops I had to jump through to make it work, and they were small and hard to see. And on fire.

The other fun thing about fetch is that while promises, XHR or $.ajax will trigger some sort of “error” or fail callback on failure status codes, fetch does not. Its error state is only triggered by an actual network failure. A 4xx or 5xx error triggers its success state.

The obvious reason for this is that it’s a joke that simply got out of hand and no one knows how to admit it. Thankfully there is a ray of sanity sunshine in the form of response.ok which does return false for these status codes. This gives you a chance to handle the… failed successes.

Fetch runs on any current browser, but doesn’t run on IE. There are polyfills easily available, or you can just let them have the crappy experience they deserve.

Async/Await

First of all, like Promises, Async/Await doesn’t actually do any requesting on its own. What it does is add some sanity to promises, allowing async operations to look and act like they’re synchronous. This meshes much better with how developers want code to look and how they expect to read it, not to mention stripping down a lot of boilerplate.

It works especially nicely with Fetch. If you remember all the “then” we had to nest, that goes away now.

let response = await fetch('api/projects');
let data = await response.json();

Not only is it less than half the lines, but more importantly it’s much more readable.

In theory, Async/Await is the most cutting edge feature listed here. In practise, though, it supported by everything the above features are. Which means no IE. There’s no polyfill here, either. Typically this feature is used in ES6+ markup and then babel will strip it out because we can’t have nice things. At least it’s nicer to write, though.

Wrappers and Libraries

Building on the previous are a bunch of libraries. Most of these make use of one or both or all of these building blocks, but make them a bit more user-friendly. Probably most importantly, though, they also polyfill missing functionality for drivers.

jQuery’s $.ajax

The eternal jQuery wraps up a lot of the above into a package that’s well known, if poorly used. The fact is the central $.ajax() method is really overused, and 90% of the time higher abstractions are way easier.

$.getJSON('api/projects', function(data){
displayProjects(data)
});

Posts are similarly cleaner. Using $.post removes a lot of unnecessary option setting and gives a very clean chunk of code.

$.post('api/projects', $('#my-form').serialize());

jQuery is much maligned these days. Modern jQuery is fully 100% Promise standard compliant, and can be used with .then instead of its success callbacks for async queries quite happily. I’ve never actually tried using it with Async/Await, but it should work perfectly fine, as it’s just a promise.

The ajax methods in jQuery are effective and current, and shouldn’t be rejected if you’re already using jQuery for something else. That’s not to say you should pull in jQuery just to use them, though. There are other options for that.

Axios and related

Axios has quickly become the http default for standalone applications or frameworks that don’t make their own http implementation standard.

It’s very straight-up http implementation using promises.

axios.get('api/projects').then(response => {
let data = response.data;
});

The previous simplifications with async/await will apply here, too.

Post works as expected.

axios.post('api/projects', formData);

You’ll note that the formData used here isn’t the same as the jQuery version and I’ve hand-waved the creation of that. It’s a standard JS object, so making it isn’t hard, but losing things like .serialize is the cost of not using a general purpose library like jQuery.

By and large, all http libraries will follow this same general pattern.

Observables

Angular takes things a new step up. Observables are a complex new way of handling data, that can be best thought of as a promise that can be resolved multiple times.

Observables are enormously powerful, but difficult to demonstrate here. This is for two reasons. One is that the benefits are only really seen over systems that need multiple accesses to the same data, not just in a simple request, but an application that deals with the data from those requests.

The second and more pressing issue is that I have no idea how they work. The only time I’ve ever used them was such a trivial case I just did this and got on with my life.

let request = await this.http.get('api/projects').toPromise();
return request.json() as Project;

That last bit is just some TypeScript, but you can see there’s nothing fundamentally different here to the axios once converted to a promise.

This Observable thing doesn’t really belong here, it’s not just a way of getting access to data, it’s a completely different way of thinking about and interacting with the flow of data through a complex application.

I highly recommend reading more about it if you’re interested.

Conclusion

There are a ton of different ways to access data. Don’t be afraid to stick with jQuery’s ajax methods if you’re already using jQuery. There’s really nothing wrong with them.

That said, you absolutely must have a handle on promises to work in a modern JavaScript application.

I feel like in 2018 we’ve hit the point where we should no longer be catering to IE in any way. If you can afford to drop IE support and/or are ok with a polyfill, you might also consider the fetch library. It’s worth pointing out that there are several patterns of libraries that are abstractions on the moderately dicky fetch API. Aurelia’s data access library is a terrible one, but Ember Data is an excellent one.

Last but not least, if you’re doing a lot of async calls, especially if you’re nesting them, absolutely look at using async/await syntax. Your code will become simpler, and more readable. This genuinely can help prevent and eliminate bugs, due to the change from the somewhat inverted layout of a chained promise.

--

--

Matt Burgess

Senior Web Developer based in Bangkok, Thailand. Javascript, Web and Blockchain Developer.