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:

  1. Append to the DOM (appending undefined!)
  2. Make an API request
  3. 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:

  1. Callbacks:
    The original way to handle asynchronous code by nesting functions
  2. Promises:
    A new feature added to JavaScript in 2015 to deal specifically with asynchronous code
  3. Async-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.