How does code actually run in JavaScript? (Part 2 — Asynchronicity of JavaScript)

Yasin Ulusoy
Beyn Technology
Published in
9 min readNov 30, 2022

In this article, we will focus on how JavaScript engine executes asynchronous operation. In my previous article “How does code actually run in Javascript? (Part 1 — Synchronicity of JS Engine)”, I explained how it executes synchronous operations and what are the fundamental parts of JavaScript engine. If you missed Part 1, you can check out here

As it is explained in the previous part, JavaScript is single threaded, and the code is synchronously executed which means each line of code is run in order the code appears. But if we need a task such as accessing an external data source, and it takes a long time? And let’s make it more complicated, what if we need to run code which uses data from an external server and if we have very slow internet connection?

If you don’t have any experience with JavaScript and if you just read part 1, you will probably say that “We have to wait and we can’t do anything until data comes from external source”. In other words, it will be blocked. The stuff I will explain below rushes to help us at that moment.

JavaScript is not enough for these kinds of tasks. So, we need to add new pieces to the puzzle (which are not JavaScript at all). We can list these new pieces as

  • Web Browser APIs / Node background APIs
  • Event Loop, Callback/Task queue
  • Promises

Web Browser APIs/Node Background APIs

As I mentioned in part 1, originally, JavaScript was designed to run scripts on websites. And browsers have plenty of available web APIs. These are not part of the JavaScript language itself, rather they are built on top of the core JavaScript language, providing you with extra superpowers to use in your JavaScript code.

As it is mentioned in Mozilla Developer Network, Browser APIs are built into browser and are able to expose data from the browser and surrounding computer environment and do useful complex things with it. For example, the Web Audio API provides JavaScript constructs for manipulating audio in the browser — taking an audio track, altering its volume, applying effects to it, etc. In the background, the browser is actually using some complex lower-level code (e.g., C++ or Rust) to do the actual audio processing. But again, this complexity is abstracted away from you by the API.

While we code in JavaScript, we use many browser APIs knowingly or unknowingly. As a JavaScript developer, I can clearly say that browser APIs I used very frequently are “Timer (setTimeOut, setInterval), HTML DOM (document), Fetch API (fetch), Web storage API (localStorage, sessionStorage, indexedDB), etc.”

I am 100% sure that these browser APIs are not strange for many of us, but the main purpose of this article is explaining how JS Engine interacts with these APIs. Let me explain it with an example.

When we checked the code above, the expectation is that we should see “Hello” in the first line, and then we should see “hi!” in the second line of the console because of the synchronous structure of JavaScript. But when we take browser APIs into account, things will get complicated. Let’s check out the scheme below.

As you see on the scheme above, because of the thread of execution, in the first line, the js engine stores the greet function to memory, and then in line 5, sends to the browser a message for the timer API with the duration and command to be executed when duration will finish. At that moment, the timer will start on the browser. After that step, the JS engine will execute the line 7, and “hi!” will be visible on the console. When, the given duration, 1000ms is completed, the function in the on completion part of the timer will be executed by js engine and “Hello” message will be visible on the console. In other words, although the setTimeout function was called before, instead of waiting execution of that function, console.log(“hi!”) in line 7 will be executed immediately and “hi!” message will be visible at first. That is the common example of asynchronous execution in JavaScript.

Callback Queue and Event Loop

Let’s make things more complicated and instead of the previous code block, check out the one below.

As you see above, at this time, I added a function veryLongIteration which will block the call stack during 1 second and time argument of setTimeout function 0ms. Let me explain how that code block will be executed. Because we focus on time, I will explain what is happening in the timeline.

Before execution: greet function and veryLongIteration function are stored in the memory.

0ms: setTimeout function is called and the web browser is triggered for the timer. Because the duration argument of the timer is 0ms, as soon as the browser triggers, the task on the browser is completed.

Before continuing to explain furthermore, let me give you a brief about callback queue and event loop. You can imagine that Callback Queue is a task queue in which tasks are waiting for jump to call stack to be executed. When there is a task which comes from the browser and if the global execution context has still included some code waiting for being executed or if the call stack is busy at that moment, these tasks are waiting in the callback queue. On the other hand, you can imagine Event Loop as a kind of cycle which checks global execution context and call stack whether they are still busy or not and whenever conditions are available, it sends the tasks in the callback queue to call stack.

In the light of information about callback queue and event loop, when the timer on the browser is completed. Callback function of setTimeOut, greet(), is sent to the callback queue to be waited for being sent to the call stack.

1ms: As it is explained above, because the global execution context has still included some codes waiting for execution, the greet function should continue to wait in the callback queue. And the thread of execution will continue to the next line including calling the veryLongIteration function. The veryLongIteration function is assumed to include a heavy iteration process which will take 1 second. Because it is a synchronous process, while it is executed, global execution context will be blocked until execution is completed.

1001ms: If you are expecting that it is greet function’s turn, you are wrong. I already mentioned a rule about callback queue that if the global execution context includes any code waiting for being executed, the event loop won’t send any task in the queue to the call stack. Therefore, we continue with the line 13 and console.log(“hi!”) will be executed.

1002ms: Finally! There isn’t any code waiting to be executed in the global execution context and the call stack is empty! Now the event loop can move the greet function from callback queue to call stack, and we can see “hi!” message on the console.

When I first heard that structure, I couldn’t believe that the callback function of setTimeout is executed with that kind of latency, and I needed to check it out. But JavaScript provides asynchronicity with the help of Timer API in that order. In order to schematize the structure, you can check the image below.

Promises

A way to handle asynchronous tasks coming with ECMAScript 6 (ES6), later renamed to ECMAScript 2015.

According to Mozilla Developer Network, “a promise is a proxy for a value not necessarily known when the promise is created. It allows you to associate handlers with an asynchronous action’s eventual success value or failure reason. This lets asynchronous methods return values like synchronous methods: instead of immediately returning the final value, the asynchronous method returns a promise to supply the value at some point in the future.”

To better understand, I can give an example. Imagine that you want to stay in a hotel during Christmas and you know that there isn’t any empty room during that time of the year. In order to stay at that hotel at that time you have two options. First one is booking a room earlier. And you don’t have to worry about your place. You already guaranteed your room. Second one is asking for an empty room when you arrive at the hotel and waiting for an empty room. As you already guessed, if you choose the second option, you will be blocked until get a room. Promises are something like first option, you can set a place for future value of a variable, a state, etc. in the memory and until that value will be provided, you can keep going with the following part of the code.

Let me give you another example. As I mentioned in the first part, in order to make a network request, we use a web browser API which is “fetch”. For me, fetch is very powerful keyword and provides very very important feature for web development. On the other hand, the fetch function has two facades. While it requests data from an external source, as soon as it is called, it returns an object to JavaScript part. In addition to being a regular JS object, it has some extra properties which are hidden properties. These properties are relatively “onFullFilled” and “onRejection”.

Before giving an example with the code piece, let me explain two concepts about promises. As I mentioned earlier, a promise object has two hidden properties which are onFullFilled and onRejection. These properties are there to define what happens when the fetching is successful and what happens if the fetching fails, respectively. At the beginning, values of these two properties are empty arrays. To give a value to these properties. We use two methods, “then( )” and “catch()”.

The “.then()” method can take two arguments; the first argument is a callback function for onfullfilled which means the promise completed successfully. Second argument, which can be used instead of “.catch()”, is a callback function for onrejection. Each “.then()” returns a newly generated promise object, which can optionally be used for chaining.

Let me explain the promise object with the code below.

As you can see from the schema above, before the JS engine starts to execute, it stores the greet and the catchError functions on the memory. And as you see on line 9, on the right side of the equality, the fetch function is called. As it has been already mentioned in the first part, an execution context will be created.

As soon as the execution context is created, the fetch function will create a promise object with empty onFullFilled and empty onRejection. Then, in line 11, “then” method and “catch” method provide values sequentially for onFullFilled and onRejection properties.Important part during that process is while these stuffs are operated in the JS Engine part, On the browser part, network request is already started. That is the “two facades” of the fetch function.

When the network request is completed successfully on the browser part, the data coming from external source is assigned to the “value” key of the promise object and the greet function which is the onFullFilled property of the promise object will be moved to the call stack and then, call stack will execute it . If the network request fails, the same process will be operated for onRejection property.

To Sum Up…

Although each point explained above contains very detailed processes in itself, they can be summarized roughly like that. Although the JS Engine was originally designed to execute tasks synchronously, it is more powerful, more robust with these asynchronous features. Therefore, the popularity of the JavaScript language increases day by day and we can create not only web apps or web sites with JavaScript but also a huge number of different products.

Sources

Introduction to web APIs — Learn web development | MDN

Learning the Hard Parts of JS | Frontend Masters

Promise — JavaScript | MDN

The event loop — JavaScript | MDN

--

--