What does asynchronous mean, exactly? Why does it matter? What does it mean for JavaScript? How do I "use" asynchronous programming? I hope to answer all of these questions in this chapter. The theory may seem a bit dry and daunting, but I Promise
(you might not get that joke yet, but you will!) that once you've got the gist of asynchronicity, you will have a much deeper mastery of JavaScript, especially with AJAX.
JavaScript is synchronous and single-threaded
…wait, what? 🤔
That means that your JavaScript code runs line by line, each line needing to be completed before the next one starts (in what is called the event loop). This means that while one piece of JavaScript code is running on a web page, for example, no other piece can be running at the same time.
Take this example:
console.log('Calculating 500th Fibonacci number...');
let a = 1;
let b = 1;
let temp = 1;
for (let i = 0; i <= 500; i++) {
temp = b;
b = a + b;
a = temp;
}
console.log('Answer is: ' + b);
What happens here is:
the first console log is shown
each variable is initialized in turn
the
for
loop runs until completion, setting the 500th number in the Fibonacci sequence tob
the second console log is shown
This seems to make a lot of sense, right? One line completes, the next line begins, etc.
But, as you will have figured out by now, not everything in JavaScript is synchronous.
Sometimes, things take time
Think back to our AJAX requests from the previous part of the course. To send a request, we did:
apiRequest.send();
All that line does is send our request. Our script will begin the sending of the request and then move straight on to the next line. It cannot take into account how long that request may take to complete. There may even be server issues which mean that we never receive a response. What this means is that we cannot have a line immediately after it which relies on the request having finished.
Therefore, our request is asynchronous, i.e. our code will continue executing whether or not the request is complete.
Let's try out an example of this.
Practice!
Head to CodePen Exercise P3CH1a and follow the instructions below.
We're going to use the setTimeout()
method to simulate a server taking time to approve our request. This should make obvious the inherent difficulties in dealing with asynchronous code (don't worry, we'll also look at several solutions!).
Start by adding a variable called
approval
, with value'Not approved!'
.Now create a function called
getApproval()
which will, after 500ms, set the value ofapproval
to'Approved!'
.Add a line calling the
getApproval()
function, and add a line immediately afterwards which sets the text content of theresult
heading to the value stored inapproval
.
Reading this code, what would you expect to happen? What actually happens? Is it what you expected?
Once you've given it a go, watch me code a solution and see how your approach compares:
How do we solve the issue of having code that takes time? In other words, how do we deal with asynchronous code?
Callbacks
The old school solution is the callback function. This is a function passed as an argument to an asynchronous function, and is called once all the asynchronous stuff is out of the way. Let's set up a callback in our example to see how it works.
Practice!
Head to CodePen Exercise P3CH1b and follow the instructions below.
To add a callback to our
getApproval()
function, we need to be able to pass a function as an argument. Change the function declarationfunction getApproval()
tofunction getApproval(callback)
.Now let's add a call to the callback function inside
getApproval()
. We want it to be called once our request is'Approved!'
, so add a function call tocallback()
just after we modifyapproval
.Now we want to leverage our now asynchronous
getApproval()
function to set ourresult
's text content once our request is approved. Remember, what we put in the callback is what we want to happen once the asynchronous stuff is complete. Therefore, we are going to callgetApproval()
and pass it an arrow function that sets our text content. Modify ourgetApproval()
call to:getApproval( () => { result.textContent = approval; } );
Was your request successfully approved?
Once you've given it a go, watch me code a solution and see how your approach compares:
As I mentioned above, callback functions are an older way of doing things. In fact, they're often baked into JavaScript.
For example, when we set the onreadystatechange
function for an XMLHttpRequest
, what we are in fact doing is declaring a callback that is executed every time the readyState
property changes. Equally, the function you set as an argument to setTimeout()
is also a callback, as it is executed after X milliseconds.
However, using callbacks can quickly lead to a place affectionately known as "callback hell," with lots of nested and hard-to-read code (there is a website called callbackhell.com which neatly describes the concept, if you are interested). Therefore, we were all very happy when we learned about the invention of promises.
Promises
Promises can be quite tricky to understand (or they were for me to begin with), so let's take things step by step.
Let's say we want to retrieve someone's name from a server. Once we have retrieved it, we want to build a string, something like 'Hi ' + name + ', how are you today?'
. That request will take some time (it is asynchronous), so we can only use name
once we are sure we have it. What if we could promise that name
will exist?
To use promises in our code, we get our asynchronous function to return a promise which will resolve
with the retrieved data once it has been received. It can also reject
if an error occurs. Then, when we call our asynchronous function, we can use .then()
to run code after the promise has resolved (with the received data) and .catch()
to handle any errors that may occur.
In theory, this all seems a bit messy, so let's apply a promise to our previous example to see how it all comes together.
Practice!
Head to CodePen Exercise P3CH1c and follow the instructions below.
As we saw in the theoretical discussion of this chapter, an asynchronous function working with Promises needs to return a Promise. Let's see how that works.
Empty out our
getApproval()
function and, for now, simply replace the contents with:return new Promise();
.The constructor for a
Promise
takes a function as an argument. That function takes two arguments:resolve
andreject
. Pass an arrow function to our Promise constructor:return new Promise((resolve, reject) => { });
Now instead of modifying the variable directly within the
getApproval()
function, we're going to have its Promise resolve with'Approved!'
after 500ms. Rebuild asetTimeout()
for 500ms, within whose callback you will write:resolve('Approved!');
. So when we rungetApproval()
, it returns us a Promise which resolves after 500ms.We now need to use a
.then()
block to use the data resolved within that Promise. Let's modify our call togetApproval()
and how we setresult.textContent
as follows:getApproval().then( (resolvedApproval) => { result.textContent = resolvedApproval; } );
The function we pass to our
.then()
block receives the data we pass to ourresolve()
function, and the whole thing works!
Once you've given it a go, watch me code a solution and see how your approach compares:
Promises already make things much tidier than callbacks. However, we can get into more messy code when things start to nest (although there are methods for cleaning these up a little). Therefore, there's a much newer way to code asynchronously.
Async/await
No two words have made JavaScript developers smile as much as async
and await
. These two little words allow us to suspend execution of our code while we wait for asynchronous code to finish.
Async/await still uses promises, but allows us to handle them in a more readable and easier to maintain manner. Let's try this out with our example.
Practice!
Head to CodePen Exercise P3CH1d and follow the instructions below.
Seeing as async/await is a different way of handling Promises, we don't have to make any modifications to our getApproval()
function. What we need to change is how we call that function and handle the data from it.
Completely remove our current call to
getApproval()
.We are now going to create a function that uses async/await to set our approval text. Declare a new asynchronous function called
setApprovalText()
as follows:async function setApprovalText() { }
.We need the
async
keyword to be able to useawait
within the function body to wait for our Promise to resolve. Within the newasync
function, declare a constant calledapprovalPromise
which holds the Promise returned bygetApproval()
:const approvalPromise = getApproval();
.Now comes the cool part: we are going to use the
await
keyword to assign the eventual resolved value from the promise to ourresult
element's text content. Add the following line to theasync
function:result.textContent = await approvalPromise;
.Now all we need to do is call the function. Call
setApprovalText()
.
Once you've given it a go, watch me code a solution and see how your approach compares:
While async/await does not gain us much in this very simple example, you will soon see how it makes potentially complex code far easier to read and, therefore, to maintain.
Let's recap!
In this chapter, we covered the concept of asynchronous code: what it is, and how we handle it using three main techniques:
callbacks: the most old-fashioned technique, but still an essential one to recognize and understand,
promises: possibly the most widely used technique today, they can seem complicated but are absolute life savers,
async/await: an easier-to-read way to handle promise manipulation.
In the next chapter, we will explore a new kind of asynchronous HTTP request.