What is asynchronous JavaScript?
What is asynchronous JavaScript, and why is it important?
In this article, we take a deep-dive to provide practice-oriented answers to these questions.
What is meant by ‘asynchronous JavaScript’?
Prelude: synchronous JavaScript
For the most part, JavaScript code is executed ‘synchronously’. This means that the JavaScript engine executes code line-by-line, starting at the top, working its way to the bottom of your script. In doing so, it doesn’t move on to the next line of code before it has completed the previous line.
So, for example:
function addOne(x) { number++; } let number = 1; addOne(number); addOne(number); console.log(number); // 3
When JavaScript encounters any line of code in the above script, it executes it before moving on to the next line. This includes going inside of functions when they are called. So, when it encounters an instance of addOne(number)
, it goes inside this function, executes it, and then returns to the script to execute the next line of code after the call.
This is why JavaScript is sometimes referred to as ‘single-threaded’: because it executes one line of code at a time in a singular process.
Asynchronous JavaScript
Asynchronous JavaScript refers to specific bits of code that are recognized by the JavaScript engine as taking some time to complete. For example, setTimeout
, which executes a function with some time delay, is an example of asynchronous JavaScript.
When JavaScript encounters such code, it is ‘put to one side’ to be returned to later. Precisely, later means when the rest of your code (the ‘main thread’) is finished.
We can see this in action in the below example by calling setTimeout
with a delay of 0:
setTimeout(function() { console.log(number); },0) function addOne(x) { number++; } let number = 1; addOne(number); addOne(number); console.log(number);
You might expect the setTimeout
function to log undefined to the console. But it actually logs 3. This is because JavaScript set it to one side, executed the main thread and then returned to it.
Why is it so important?
setTimeout
is one example of asynchronous JavaScript. But asynchronous JavaScript is mainly important because there are so many processes we run that take some time for which JavaScript doesn’t wait. This includes any website that loads resources from a server.
Some examples of asynchronous JavaScript:
- HTTP requests (GET, POST, PUT, DELETE)
- Loading an image or script
- Database interactions
- setTimeout
HTTP requests cover a wide range of actions, including the use of JavaScript’s fetch API, jQuery’s $.ajax() method or Axios-based requests. In backend development with Node.js, database communication is not instantaneous and is therefore treated asynchronously by the JavaScript engine.
That JavaScript doesn’t hang around for processes it knows will take some time to complete is actually a good thing! It makes the execution of your code faster by not ‘blocking’ the loading of the web page dependent upon some process completing that is not necessary for generating the main content.
But it does present us with some programming challenges, especially when we want to ensure that some of our code ‘waits’ upon the completion of an asynchronous process.
The challenges of asynchronous JavaScript
The main problem of asynchronous code: What if later parts of our code should only execute after the completion of some asynchronous code? This is very typical in modern web development, where we often only want to append content to the DOM after it has been retrieved from a server (asynchronously).
Below is a simplified example, where setTimeout
is representative of such a delay (e.g. HTTP request) and console.log()
; the handling of the result (e.g. appending to the DOM):
let data; setTimeout(function() { data = "Some Data"; },1000) console.log(data); // undefined
Because JavaScript deals with setTimeout
out by putting it to one side, executing the rest of the code and only returning to it later, when console.log(data)
is executed, "Some Data"
has not yet been assigned to the data
variable, and it therefore returns undefined
.
This example could be solved by simply placing console.log(data)
inside the callback function:
let data; setTimeout(function() { data = "Some Data"; },1000) console.log(data); // undefined
But real-world scenarios are often more complex.
For example, imagine we have three tasks that we want to execute one after the other:
setTimeout(function(){ console.log("First task") },700); setTimeout(function(){ console.log("Second task") },1500); setTimeout(function(){ console.log("Third task") },500); // Output: // "Third task" (after 0.5 seconds) // "First task" (after 0.7 seconds) // "Second task" (after 1.5 seconds)
Despite writing them in one order, they complete in a different order: from lowest to highest countdown.
If each result is independent of each other, this may not be such a big deal.
But sometimes we want task two to run only once task one is complete. And task three only after task two is complete.
For example, our first task may be an API request, our second task some processing of the result (e.g. from JSON to a JavaScript object) and the third task appending to the DOM. So in this case we would:
- Append to the DOM (appending undefined!)
- Make an API request
- Process the result
Obviously, this would cause a major problem!
A question of timing
In the examples above, we set a fixed time for the completion of each task. But this is an unrealistic assumption. In reality, we often don’t know when a process will resolve or even if it will resolve!
To make it more realistic, each setTimeout
should take a short but random amount of time to complete and there should be a low but non-trivial possibility of the API request failing:
setTimeout(function(){ const randomNumber = Math.floor(Math.random() * 10); if (randomNumber == 1) { console.log("API request failed"); } else { console.log("API response received"); } },Math.random() * 1000); setTimeout(function(){ console.log("Process API response"); },Math.random() * 100); setTimeout(function(){ console.log("Append processed response to DOM"); },Math.random() * 50);
Run this code several times, and you will notice that the processes do not always finish in the same order and sometimes the API request fails.
This is the reason we need something other than setTimeout
: we do not know how long processes will take to complete, and we would like each processes to begin as soon as the previous one is complete.
To handle asynchronous code, we need asynchronous programming techniques!
Asynchronous programming techniques
Several tools are available to us to handle asynchronous code:
Callbacks
:
The original way to handle asynchronous code by nesting functionsPromises
:
A new feature added to JavaScript in 2015 to deal specifically with asynchronous codeAsync-await
:
A ‘cleaner’ syntax for working with promises introduced in 2017 (still promises ‘under the hood’!)
Generally speaking, promises are considered the ‘modern’ way to deal with asynchronous code (along with async-await) but callbacks are still widely used.
Summary
Asynchronous JavaScript refers to those bits of code that the JavaScript engine recognizes as taking some time to complete, so they are ‘put to one side’ and returned to once the rest of a script is executed.
We also learnt that there are three ways to deal with asynchronous code: callbacks, promises and async-await.
Related links
- Mozilla MDN Docs: Asynchronous JavaScript
- Geeks for Geeks: Synchronous and Asynchronous in JavaScript
- Stack Overflow discussion: Asynchronous vs synchronous execution, what is the main difference?