As we all know, JavaScript is a single-threaded language. If there is simple synchronous code then code execution happens one after the other and it's all a piece of cake. In reality however, we often find ourselves dealing with asynchronous events and we want JavaScript to handle these events in a non-blocking manner so that our code is not stuck while waiting for an async event to complete. Therefore, to provide asynchronous capabilities, the following data structures are used within browsers to make it work seamlessly.
The Call Stack
The call stack is used to manage the execution contexts for your JS code. It is handled by the JS engine, which creates the contexts, pushes it into the stack, executes code and pops the contexts out.
There are basically two types of execution contexts:
Global Execution Context: When your JS code starts executing, the JS engine creates a GEC and pushes it into the call stack. There is only one GEC created which then executes your JS code from top to bottom, line by line. After reaching the end of your code, this GEC is popped out from the stack.
Function Execution Context: When the JS engine comes across a function call in your code, it creates a new execution context for it and pushes it into the call stack. There can be many function execution contexts in the call stack as a new one is created for every function.
Any code that has to be executed has to be put into the call stack first.
The Task Queue or Callback Queue
When the JS engine comes across some asynchronous code that is blocking in nature (could be some Web APIs like setTimeout or an event listener), a callback function is registered in the Web APIs environment. This callback function cannot be put into the call stack directly so the Web APIs push them into an alternate area where it can wait for the call stack to be ready. This alternate area is called the callback queue or the task queue.
Therefore, once the timeout has elapsed or the event has occurred, the callback function is pushed into the callback queue by the Web APIs. Once the call stack becomes empty and available, the callback from this callback queue is pushed into the call stack by the Event Loop.
The Microtask Queue
Some Web APIs return a promise upon completion of an async computation for example: fetch() call which makes a network request to a third-party and returns a promise. The callbacks coming from such Web APIs are again registered in it environment but they are not pushed into the callback queue. These callbacks are pushed into a separate queue called the microtask queue. The microtask queue takes higher priority than the callback queue, meaning all the callbacks in the microtask queue will be executed first then only the next callback from callback queue will start executing. (by executing, I mean being pushed into the call stack).
The microtask queue therefore is responsible for holding high-priority callbacks that need to be immediately executed as soon as the call stack becomes ready.
These three data structures combined together with Event Loop and Web APIs enables JavaScript to handle asynchronous programming albeit being a single-threaded language. Truly wonderful, isn't it?
Well, thanks for reading and I hope it helped you. See you in the next one! ✨