Navigating JavaScript’s AJAX Landscape

The solution to populating dynamic data with JavaScript has seen many phases and is continuing to evolve. A database, an API, a cache, a hard drive or anywhere else data maybe hiding all have one thing in common when it comes to JavaScript and Node: you have to write code that will execute asynchronously to interact with them. This article will give a brief overview of how getting and sending data has evolved over the years, the current options we are working with, and how to leverage each.

Solutions to the asynchronous data grabbing have evolved over the years, the XHR library, being one of the earliest ways to get data outside of what is provided in the document the JavaScript was executing in, JQuery provided a wrapper around this, and other libraries made it easier to use this API. The reader unfamiliar with what AJAX and XHR are or how asynchronous action works in JavaScript should start with the XHR library before moving onto more modern APIs and libraries. Learning the foundation of AJAX will make everything else make much more sense later.

Eventually things like the Promise API were created, and following them fetch, async/await, and libraries like Axios. I’m sure as I type this article someone is inventing a new way to deal with getting data in JavaScript.

The solutions to these problems have evolved, but the problems themselves have stayed the same. When executing JavaScript code we need to wait to get the data back before moving on. In the early days of XHR we would write something like this:

let req = new XMLHttpRequest();
req.onreadystatechange = function () {
if(this.readyState === 4 && this.status === 200){
let node = document.getElementById("elementwewanttoputdatain")
node.innerHTML = this.responseText;
}
}
req.open("GET", "theplaceyouwanttogetdatafrom.com", true)
req.send();

Why do we have to write all that crazy stuff in the middle and then check all these variables for stupid numbers? Why can’t we write something like this?

let req = new XMLHttpRequest();
let data = req.open("GET", "theplaceyouwanttogetdatafrom.com", true)
let node = document.getElementById("elementwewanttoputdatain")
node.innerHTML = data

Because by the time we hit the line where we are assigning node the line where we are assigning data has not yet finished executing and data is still undefined.

Skipping a number of steps along we eventually got to the point of being able to write something like the above. The recently created async functions combined with the ‘await’ operator allow us to write stuff like this.

async function someasyncthing(){
const data = await doSomeAysncThing();
let node = document.getElementById("elementwewanttoputdatain")
node.innerHTML = data
}

or using the fetch API without async/await

fetch("theplaceyouwanttogetdatafrom.com")
.then(data => {
let node = document.getElementById("elementwewanttoputdatain")
node.innerHTML = data
})

I haven’t taken time to explain all these concepts so far, just introduce them, so if the reader is not familiar please stop reading and learn more about them. The point of this article is suggestions on how to juggle these solutions. But… wait… why would anyone want to use anything other than Async/Await, it gets ride of the need for awkward “thenning” and catching and looks like what we as developers kind of wanted in the first place.

When one starts writing in the real world with error handling and multiple data transformations, one can start to write code that looks like this.

async function someasyncthing(){
let node = document.getElementById("elementwewanttoputdatain")
try{
const data = await doSomeAysncThing();
}catch(e){
node.innerHTML = "HORRIBLE ERROR"
}

Which looks barely different from.

fetch("theplaceyouwanttogetdatafrom.com")
.then(data => {
let node = document.getElementById("elementwewanttoputdatain")
node.innerHTML = data
})
.catch(error => {
node.innerHTML = errorl
})

The difference is minor, so it is tempting to just always go with async/await, but there are times when the clarity of the “then” way of writing things can make code more readable and self-documenting. When there is only one ‘then’ block it can be easier to write it in the Promise manner, without async/await. When in a situation like this, however:

async function someasyncthing(){
let node = document.getElementById("elementwewanttoputdatain")
try{
const data = await doSomeAysncThing();
}catch(e){
node.innerHTML = "HORRIBLE ERROR"
}
try{
const dataNeededFromPrevData = await doSomeDifferentAysncThing(data)
}catch(e){
node.innerHTML = "HORRIBLE ERROR OF A DIFFERENT KIND"
}
if (dataNeededFromPrevData.length > 3){
try{
const dataWeNeedNow = await doSomeAysncThing(data);
}catch(e){
node.innerHTML = "HORRIBLE ERROR OF AN EVEN DIFFERENT KIND"
}
}else{
try{
const dataWeNeedNow = await doSomeAysncThing(dataNeededFromPrevData);
}catch(e){
node.innerHTML = "HORRIBLE ERROR OF AN EVEN DIFFERENTERRRR KIND"
}
}
if(dataWeNeedNow){
let node = document.getElementById("elementwewanttoputdatain")
node.innerHTML = data
}
}

It might be better to go with then for readability sakes, because of can just follow the thens and debugging becomes easier because one can just find the first place data is null, and if in trouble one can always just lean on a handy “catch” block. The added parachute of every “then” having a “catch” is also really helpful in debugging.

The final thing one could try is some sort of helper function or library to deal with this try/catch problem. One could write a function that takes in a promise and returns an object with an “error” property and a “data” property, and that would eliminate try/catchs and just allow for checking those properties. There are a few things to augment async/await and fetch to make it more readable and get out of whatever the newest version of callback hell is, as always, following good coding principles of single responsibility functions and not repeating yourself will make it easier to deal with whatever issue one is dealing with.