Asynchronous programming with Javascript

Nipuni Yapa Rupasinghe
10 min readFeb 5, 2021

Javascript is a single-threaded language which means it only has a single call stack and only one heap (don’t worry, let’s talk about these things in a short while). Therefore Javascript can’t do multiple operations at the same time which makes javascript synchronous and blocking. But this can be a terrible thing when we have to perform so many time-consuming API calls. In such scenarios, the beauty in Javascript is that even though, Javascript is synchronous, we can make Javascript behave asynchronously. Let’s see how we can do that.

To move forward, we need a basic understanding of the Javascript runtime environment and the event loop.

Javascript Runtime Environment and the Event Loop

Javascript Runtime Environment

The above diagram shows the Javascript Runtime Environment. When we write a Javascript code and loads it to the browser it will run on top of the Javascript engine which comes with the browser. Different browsers have different Javascript engines (e.g.: Google Chrome uses V8 Javascript engine)

Inside this Javascript engine, there are two main things.

  1. Call Stack: a Last In First Out (LIFO) data structure to store the execution contexts
  2. Heap: an unstructured data structure to store function definitions

Outside (not inside the engine) this Javascript engine, from the browser, we get Web APIs, Message Queue, and the Event Loop.

Web APIs:

Out of the Web APIs supplied by the browser, what we are interested in here is the Timer Methods and more specifically setTimeout method. When you find setTimeout functions inside Javascript code, don’t get confused, as it is not executed as a part of the Javascript code. It is like a request that we are passing to these Web APIs saying that please wait this many milliseconds and then perform the task. The syntax for that call is like this,

setTimeout(the task, this many milliseconds);

NOTE: Please keep in your mind that, the setTimeout function itself is not asynchronous. When we make an API call, that API gets the arguments passed by us, then processes the request which takes some time, and then sends us the response. So we use this setTimeout function to simply stage such a scenario to understand the concepts.

Message Queue:

The Message queue is a First In First Out (FIFO) data structure which is like a waiting area and once the timer comes down to zero the task will be moved to the Message Queue (the waiting area) and waits for the call stack to become empty.

Event Loop:

The Event loop will continuously check whether the call stack is empty and whether the message queue contains any waiting task. If there are tasks in the message queue and if the call stack is empty, the event loop will move the waiting tasks to the call stack so that the tasks can get completed.

Still, no clue how this works huh? Just hang in there. Once you read the below examples, you will understand everything.

Synchronous code execution

When you take a simple javascript code it will execute synchronously line by line.

Synchronous.js

So, for this code execution, we only need the call stack and the heap. The heap will have function definitions and the call stack only keeps the addresses of these function definitions (not the function definitions)

When the line-by-line execution happens and once the 21st line of the above code gets executed, an execution context gets pushed to the stack, and function One() gets executed and once the execution is completed, the stack gets popped. Then the execution moves forward to the 22nd line and the same thing happens in there too. This will continue until all five functions get executed. Well, that’s easy.

And the output of the above code is,

The output of Synchronous.js

Asynchronous code execution

Let’s add some setTimeout functions to the above code to get the feeling of an asynchronous code.

Asynchronous.js

Now here we need everything that we discussed above. To make it more clear let’s go step by step. I suggest you to draw these steps on a piece of paper to have a good understanding of what is happening here.

STEP 01: Line by line execution hits line 21 and Function One() gets executed similarly as explained in the synchronous code.

STEP 02: Line by line execution hits line 22, execution context for function Two() gets pushed into the stack and the execution of function Two() gets started.

STEP 03: Now there is a setTimeout function inside function Two(), therefore, setTimeout function gets pushed to the stack and starts its execution. When executing the setTimeout function, it will pass console.log(2) to the Web APIs and ask to start the timer and wait for 4000 milliseconds.

STEP 04: Once the setTimeout function completes its execution it will get popped from the stack and with that function Two() completes its work and gets popped from the stack.

STEP 05: Line by line execution hits line 23 and a similar thing explained in STEP 02, STEP 03, and STEP 04 above will happen with function Three()

STEP 06: Lines 24 and 25 respectively get executed similarly as line 21.

Meanwhile, the two timers will work in the background and as they hit zero, the console.log()s will move to the Message queue in the order the timers hit zero. In our example, 0 milliseconds will hit zero (well it's zero already) before 4000 milliseconds. Hence console.log(3) will be at the head of the message queue followed by console.log(2)

IMPORTANT: Here I used 0 milliseconds to stress another point. Once we call the setTimeout function with say, 4000 milliseconds, it doesn’t mean the execution will happen soon after 4000 milliseconds has passed. It simply means that the timer should wait that many milliseconds before sending the task to the Message Queue. So when you pass 0 milliseconds to setTimeout function don’t misunderstand, it won’t execute right away as it's 0 milliseconds. Once you call setTimeout function, you definitely have to wait for the call stack to be empty before executing the relevant callback function (which is passed along with the setTimeout function).

STEP 07: Once the call stack is empty, the event loop will check the Message Queue and move the task at the head of the queue to the call stack which is console.log(3) in our case. It gets executed and the stack gets popped.

STEP 08: As again the call stack is empty, the Event Loop will do the same, and now console.log(2) gets executed.

As now both the call stack and the message queue are empty, we can say that we have completed the execution of the Asynchronous.js code.

And the output of the above code is,

The output of Asynchronous.js

Most of the time when making an API call, the Javascript code gets executed asynchronously (without waiting for the API request’s response, the code continuously gets executed). Now the biggest challenge is when we want our asynchronous code to follow a specific order of execution (when we want something to happen only after the API request got completed), how to handle such scenarios? The answer is to use callback functions.

Handling asynchronous code with Callback Functions

When we pass a function (A) as a parameter to another function (B), A is called a callback function, and B is called a Higher Order Function.

HigherOrderFunction(callback()){
//Some code
callback();
}

Callback functions are nothing but a type of function. Callback functions have nothing to do with the asynchronous code, but simply a feature that we use to handle asynchronous code.

Suppose in the above Asynchronous.js, we need to run function Three() only after the execution of function Two(), then we can do that like this using callback functions.

CallbackAsync.js

Now, function Two() is the Higher-Order Function and function Three() is passed as an argument (callback function) to the function Two().

When function Two() gets executed the following happens in the mentioned order.

  1. An execution context of function Two() gets pushed to the call stack
  2. An execution context of setTimeout Function gets pushed to the call stack
  3. The callback function (this is an anonymous function, for later reference let’s say function foo()) inside the setTimeout function gets passed to the Web APIs and the timer is set to 4000 milliseconds.
  4. setTimeout and function Two() execution contexts get popped from the call stack respectively
  5. The rest of the code gets executed (in a similar manner as explained in Synchronous.js)
  6. Once the call stack is empty the Event Loop will move an execution context of the function foo() to the call stack
  7. console.log(2) gets executed
  8. Then an execution context for Function Three() gets pushed to the call stack
  9. A similar thing will happen with the setTimeout function inside function Three()
  10. setTimeout, function Three() and foo() execution contexts get popped from the call stack respectively
  11. Once the call stack is empty the Event Loop will move an execution context of the function ()=>{console.log(3)} to the call stack
  12. console.log(3) gets executed
  13. the execution context for ()=>{console.log(3)} gets popped from the call stack

The output of the above code is,

The output of CallbackAsync.js

NOTE: Compare and see the difference between the codes and the outputs of Asynchronous.js and CallbackAsync.js.

Usually, when we define callback functions (for asynchronous code handling) we pass two parameters; the error, and the response. The error is the first parameter.

callback(err, res){
if(err){
//handle error
}
else{
//Process response
}
}

When we pass this callback to the API request, if there is an error, the API will pass the error as an argument to the callback function and if everything is fine (there are no errors) then the API will pass null as the first argument and the response as the second argument.

The problem with callback functions is that when we have to call multiple API requests, it gets nested, messy, and hard to follow. This is called callback hell.

ApiRequestOne((err, res)=>{
if(err){
//handle error
}
else{
//Process response
ApiRequestTwo((err, res)=>{
if(err){
//handle error
}
else{
//Process response
ApiRequestThree((err, res)=>{
if(err){
//handle error
}
else{
//Process response
}
});
}
});
}
});

As a solution to this, Javascript developers introduced promises.

Promises

A promise is an object which has member functions such as then and catch. Usually, asynchronous APIs return a promise upon the completion of the execution. Using the promises, we can handle asynchronous API calls with cleaner code rather than callbacks.

To access the return value of a promise we can use the then function of the promise object, and to handle errors, we can use the catch function.

ApiRequestWhichReturnsAPromise()
.then(res => {
//process response
})
.catch(err => {
//handle error
});

A promise can either get resolved or get rejected. In here, the then block only gets executed if and only if the promise gets resolved. If the promise gets rejected the catch block will handle it.

The previous scenario in which we got a callback hell can be handled as follows using promise chains.

ApiRequestOne()
.then(res => {
//process response
ApiRequestTwo()
})
.then(res => {
//process response
ApiRequestThree()
})
.catch(err => {
//error handling
});

This is possible because each then and catch will also return a promise. As you can see this is not messy and easy to follow. For error handling unlike in callbacks, attaching only one catch at the end of the chain will be enough (don’t need to have a catch for every then).

Even though promises helped to solve most of the problems with callbacks, Javascript developers wanted even more. They wanted asynchronous code to look as same as synchronous code. So they introduced Async Functions (with async/await keywords).

Async Functions (async/await keywords)

Defining async functions is pretty easy. You just have to add the async keyword at the beginning of the function definition. Once you add the async keyword, you are allowed to use await keyword inside the async function. Unlike other functions, async functions return a promise.

async function getData(){
const data = await ApiRequest();
//process data
}

Here, await make sure that data processing will happen only after ApiRequest() retrieved data.

The await keyword causes the async function to pause its execution but the code outside the async function will continue to execute without getting blocked.

With async functions, error handling can be done using try and catch as follows.

async function getData(){
try{
const data = await ApiRequest();
//process data
}catch(error){
//error handling
}
}

The previous scenario in which we got a callback hell can be handled as follows using async functions.

async function ApiCalls(){
try{
const one = await ApiRequestOne();
//process data
const two = await ApiRequestTwo();
//process data
const three = await ApiRequestThree();
//process data
}catch(error){
//error handling
}
}

Now the code looks similar to a normal synchronous code, even though asynchronous calls are happening inside the function.

In this article, we have learned how to do asynchronous programming using Javascript. Under that, we discussed the Javascript Runtime Environment, event loop, callback functions, promises, and async functions.

Hope this article is helpful to the people who are trying to understand these concepts. Cheers!

--

--

Nipuni Yapa Rupasinghe

Software Engineering undergraduate @University of Colombo School of Computing (UCSC)