Introduction:
Before going deep into the core of the JavaScript runtime and how async code tasks are run behind the scenes, let’s get the basics clear. JavaScript is a single-threaded language. This means it has only one call stack and one memory heap. Hence, it can only execute one code at a time. In other words, the code is executed in an orderly fashion. It must execute one code in the call stack before moving to the next code to be executed. There are two types of code tasks in JavaScript, asynchronous code which runs and gets executed after certain loading, synchronous, which gets executed instantaneously. Let us understand the difference between the synchronous and asynchronous code before moving further.
Synchronous code:
Most of the code is synchronous.
It is executed in a line-by-line fashion, i.e., each line of code waits before the previous line to finish its execution.
Long-running code operations block the code execution for further stacked code executions.
Asynchronous code:
Asynchronous code isn’t synchronous. I.e., the code is executed after a task that runs in the background finishes.
It is non-blocking in nature. Execution doesn’t wait for an asynchronous task to finish its work.
Callback functions alone don’t make the code asynchronous.
Runtime:
Runtime is the environment in which a programming language executes. JavaScript’s runtime majorly constitutes three things namely JavaScript Engine, Web API, Call stack. JavaScript can work with asynchronous code as well as synchronous code.
The unique feature of JavaScript’s runtime is that even though JavaScript’s interpreter is single-threaded, it can execute multiple codes at a time using concurrent fashion in a non-blocking way. This enables asynchronous behavior. As the interpreter is not multithreaded, it rules out parallelism. Let’s understand what is the difference between concurrency and parallelisms.
Concurrency:
Under this approach, the tasks run and complete in an interleaved fashion. I.e., the tasks run concurrently but at a given point of time, only one task is being executed. This happens when the tasks are braked into small parts and are managed pretty well. This is also shown in the figure below.
Parallelism:
In contrast, under the approach of parallelism, we can the tasks run simultaneously, i.e., at a particular point of time many tasks can run regardless of other tasks running. This happens when we multithread the tasks into different threads available for the interpreter.
Having understood that JavaScript runtime follows a concurrent fashion of execution, let us understand how different code is executed behind the scenes smartly. To understand the process of execution, we need to understand the structure of JavaScript runtime in detail.
JavaScript Engine:
JavaScript engine can be considered as the heart of the runtime. It is the place where each code is executed. JavaScript engine constitutes of Heap storage and call stack. Let’s understand each of those.
Heap :
It is the place where all the objects and data are stored. This is similar to the heap storage we see on various other languages like C++, Java, etc. It contains the store of the data related to all the objects, arrays, etc. that we create in the code.
Call Stack:
It is the place where the code is stacked before the execution. It has the properties of a basic stack( first in last out ). Once a coding task is stacked into the call stack, it will be executed. There is an event loop that takes place and this is the one that makes the JavaScript interpreter smart. It is responsible for concurrent behavior.
Web API:
JavaScript has the access to different web API’s and it adds a lot of functionality. For example, JavaScript has the access to the DOM API, which gives access to the DOM tree to JavaScript. Using this, we can make changes to the HTML elements present on the browser. Also, you can think of the timer, which gives it access to the time-related functions, etc. Also, the geolocation API which gives it access to the location of the browser. Like this, JavaScript has the access to various other APIs.
Callback Queue:
This is the place where asynchronous code is queued before passing to the call stack. The passing of the code task from the callback queue to the call stack is taken care of by the event loop. In addition to this, there is also a micro tasks queue.
Micro tasks Queue:
The microtasks queue is similar to the callback queue but has a higher priority of execution than it. In other words, if there is a situation where the call stack is empty (except the global execution context ) and there are two tasks to be executed, one from the micro-tasks queue and the other from the normal task queue or callback queue, then the code task present in the microtask queue has the higher priority than the latter.
Having understood the basic terminologies involved, let’s quickly understand how the asynchronous code work.
How does asynchronous JavaScript work behind the scenes?
Here, we get introduced to the concept of the event loop. In simple words, an event loop can be defined as a smart technique of execution of executing the code from the callback queue by passing into the call stack, once it is found to be empty ( Except the global execution context ).
The event loop decides when to execute each code task present in the callback queue and the micro-tasks queue. Let us understand the execution process of all the code in an imaginary situation. Let us try to generalize the process into different steps :
All the code tasks present in the call stack are executed in an orderly fashion. It is synchronous and waits for the previous code task to be executed. In this step, all the code tasks in the call stack are executed.
Once the asynchronous task finishes getting loaded in the background, it is sent to the callback queue. The callback function attached to this asynchronous task is waiting to be executed right here. This asynchronous is then queued up to be executed in the callback queue.
Now, the part of event loops comes into play. The event loop continuously checks if the call stack is empty and once it finds it to be empty, it takes the first task in the callback queue and stacks it into the call stack which is then executed. This process continues until the event loop finds the call stack and callback queue to be empty.
Do promises also go to the callback queue?
No, let us understand how they work behind the scenes. Promises are also a special type of asynchronous tasks which once after loading get queued up in a special place called micro tasks queue. This microtasks queue has higher priority as compared to the callback queue when execution. The event loop also checks for the tasks in the micro-tasks queue when checking for tasks to be executed in the callback queue. If it finds any task to be executed, then it gives the micro-tasks higher priority and they are executed first.
Example:
YouTube:
Let us consider the following example. In this case, there are two synchronous and two asynchronous tasks ( Read comments ). In this example, first, synchronous task 1 is sent to the callback and is executed. Then, the asynchronous task 1 is loaded in the background which is a built promise. Then, asynchronous task 2 is loaded in the background. The last synchronous task is executed asap. Then the promise is sent to the micro tasks queue, at the same time setTimeout which is an asynchronous task is loaded behind. Now, we come across a clash between asynchronous task 1 and asynchronous task 2. As the promise is sent to the micro tasks queue, it has higher priority and is sent to the call stack and is executed. Then the setTimeout is executed. Here we can see that due to the already queued up tasks, the setTimeout is delayed and the callback is executed after more than 0 seconds(the timer set).
//Synchronous task no 1
console.log("This is executed first");
//Asynchronous task no 1
Promise.resolve("This is executed third")
.then((res)=>console.log(res));
//Asynchronous task no 1
setTimeout(()=>console.log("This is executed fourth"),0);
//Synchronous task no 2
console.log("This is executed second");
Conclusion:
This is all about How async JavaScript is run behind the scenes. This may be too heavy to grasp and that’s okay. It is just that in JavaScript different types of functions have different priorities of execution and behavior. The video that I’ve attached with this nicely explains the concept. You can even try your examples and see the outputs you may get.
That’s all to this post. If you've come to this section, I appreciate it. Most developers skip this in their learning and Who knows if this is your interview question for one of your JavaScript interviews. You can always connect with me on my social handles. I’m always open to discussions on Twitter. Also, you can have my LinkedIn and mail. If you have time, please visit my portfolio and let me know your suggestions on where I can improve.
Thank you for reading my article. Meet you in the next article friends. This article would be continued further. So please follow me and stay connected. If you found this article useful, please let me know your feedback in the comments below. Also A reaction would always be appreciated.
Apart from this you can also connect with me on Twitter, LinkedIn, also GitHub. Thanks for reading this article.
Top comments (1)
Thank for this very nice article. I've learned a lot from this.
But I have a thing to contribute that is the** (macro)task** queue is high priority than microtask queue. Following your example in . The reason of the setTimeout ran after Promise is it was sent to the (macro)task queue and execute in the next iteration (Tasks added to the queue after the iteration begins will not run until the next iteration.). It is different with microtask queue when execution of microtasks continues until the queue is empty—even if new ones are scheduled in the interim.