Asynchronous Javascript
Web developers are generally knowledgeable about a lot of things. Not only are they expected to know about at least one popular framework like React, Angular or Vue, they are also expected to how to CSS and there’s the whole browser support situation which let’s just not go into right now. It can quickly become daunting, seeing all the stuff you know nothing about, even though you should.
In a sea of articles, youtube videos and documentation, it’s easy to forget about the basics. One of those basics is communicating with the servers. What are Promises? What’s the difference between async/await and .then callbacks? Who knows, right? Well, you should, and after reading through this article you will understand it much better.
What is it?
Communicating with servers is something a Front-End developer has to know by heart. Implementing a login flow, sending a form user just filled, creating an infinite scrolling list? All of these need some type of backend service to work.
Imagine asking a friend, let’s call him Charlie, what the answer to the third question on the math exam you just had was. He’ll probably need a few moments to remember it. While waiting, another schoolmate asks you if you can lend him your pen. Instead of saying “yea sure, just a sec” and taking a pen out of the backpack while Charlie remembers what the answer was, all you could do was blankly staring at Charlie until he answered. What a waste of time, huh? Well, this is how life without asynchronous calls would look in the computer world.
When you request data from the server, some time will pass before that data is returned to you. You can’t know how long that will take, because the internet is an unpredictable beast. Your user may have a spotty connection, which takes forever to load anything. Or they might be leisurely browsing the internet on one of the finest fiber connections ever to grace a LAN port. Maybe the server you’re trying to contact is just processing some other requests or is, god forbid, offline? The point is you can’t know. And while you wait, there’s plenty of other things you might have to do.
Keep Your Promises
This is where Promises come in. What? Promises! You can use them inside an asynchronous function or with a .then callback. When you issue a request to the server, you essentially create a promise that gets stored somewhere in the back of the browser JS engine, waiting for the server data to come back. Read about the javascript event loop if you want to know what’s happening under the hood. When the data is received, the JS engine resumes code execution from where the promise was made, enabling you to do something with the server’s response.
A Promise has 3 states:
- Pending: This is the state the Promise is in initially. Until something happens to it, it will remain in this state.
- Rejected: If something goes wrong (spotty connection, server died while processing your request or didn’t respond in time, the user entered a wrong password, etc…), the Promise gets rejected. It’s highly advisable to plan for this to happen because sooner or later it will.
- Fulfilled: When you request comes back with the data, the Promise gets fulfilled (often referred to as resolved), and your code execution picks up from where it left off.
You can wait for multiple promises at once, of course.
Then... What?
const getData = async() => {
try {
const response = await axios.get(‘/data)
// handle response
} catch (error) {
// handle error
}
}
You may be aware that Javascript has been going through some sort of a renaissance for a while now. Luckily, this means using Promises is easier than ever. Unluckily, there is a lot of older code out there too. This means you have to know both ways of waiting for the promise to be fulfilled. To be fair, both ways work exactly the same, so it’s up to how you go about doing it.
Code examples below use a client called Axios because it’s quite popular. You can obviously use any kind of HTTP request client and if you’re brave enough maybe even the excellent Fetch API.
const getData = () => {
axios.get(‘/data)
.then((response) => {
// handle response
}
.catch((error) => {
// handle error
}
}
So, what’s happening here? A function getData issues a request to the server, asking for, well, data. Until the server responds, the JS engine is free to do other stuff. If the server responds nicely, the Promise gets fulfilled and the .then block is executed, giving you access to the response. If the server responds with an error, the network is not cooperating, or anything else bad happens, the Promise gets rejected, calling the .catch block. There, you can log an error or maybe even display it on the screen, so your user knows something went wrong.
This is how JS request syntax looked for a long time. It has been around forever, and it’s fine. It can get complicated when you chain many asynchronous calls one after another, that’s why it can be sometimes referred to as promise hell, as chaining many promises one after another can get unwieldy pretty fast:
const getUser = () => {
axios.get(‘/user)
.then((user) => {
//save user data
axios.get('/preferences')
.then((preferences) => {
//save user preferences
axios.get('/listData)
.then((list) => {
// display the list
})
})
})
}
Uhhhh. So, the first thing you get is user data (like email and stuff), then you need to know their preferences - which lists they want to see, and only then can you finally get the lists and display them. Notice that error catchers are completely absent because that would make the code even less readable.
Now, this is obviously just an example as this flow can be implemented much better on the backend directly, but there are instances where it’s simply impossible to avoid chaining API requests.
But don’t worry, there is a better way. A much nicer way too.
A-wait..
const getData = async() => {
try {
const response = await axios.get(‘/data)
// handle response
} catch (error) {
// handle error
}
}
Carefully read the code above and notice how something called async was added to the beginning of the arrow function. It tells the JS engine that this function is asynchronous, enabling it to use the await keyword. Wait, await? Why, yes! Instead of using the old .then, you can a-wait until the promise gets fulfilled and store the response in a new variable conveniently named response. The code execution will wait until your Promise gets fulfilled, and continue when it does. Because a Promise can also get rejected, it’s advisable to use the try/catch blocks. Any errors you might encounter inside the try block will be caught and passed to the catch block. If the code encounters an error anywhere inside the try block, the execution of this block is stopped and the catch block gets run instead. There, you can, for example, display the error to the user. Be careful to log to the console as well, at least when you’re developing the app, so you, the developer, will know something bad happened.
Now, let’s see how the promise hell from above looks with the new syntax:
const getListData = async() => {
const userData = await axios.get(‘/listData)
// display the list
}
const getUserPreferences = async() => {
const preferences = await axios.get(‘/preferences’)
// save user preferences
getListData()
}
const getUser = async() => {
const user = await axios.get(‘/user)
// save user data
getUserPreferences()
}
Much better. The code is executed from the bottom towards the top because hoisting does not work for variable values. Both examples do the exact same thing, but this one is arguably easier to read.
Real-world, please
Sure!
There are a lot of free APIs for you to try out. One of them is iTunes Search API. Open this code pen where a search is set up for a well-known music artist Taylor Swift.
Once the button gets clicked, a request is issued to Apple’s servers* where it gets processed and it returns a response. If anything goes wrong, it will get logged to the console and displayed on the page as well, so the user knows something didn’t work as expected.
You can play around with this code too, for example, you could add a loading indicator so the user knows something is happening in the background.
*Because Apple’s servers don’t allow CORS, a proxy was used.
Fulfilled
Promises are a powerful tool every web developer should know inside and out. It’s a good idea to experiment with asynchronicity until it starts to become clear how it works. There are a LOT of free APIs out there for you to play with, so go do that!