JavaScript: Call Stack / Event Loop

Jumanah Al Asadi
3 min readJun 7, 2021

--

A simple interview question that tests your knowledge of how JavaScript works under the hood

Question: What is the output, or order of execution of the following code?

let num = 0;setTimeout( () => {  console.log (“set timeout”, num);}, 0)async function increment() {  num += await 2;  console.log (“await / async”, num);}num += 1;increment();console.log (“at the very end”, num);

The output in the console is in this order.

at the very end 1await / async 3 ​​​​set timeout 3

Weird right? How come the console log at the end of the script was executed first? And how come a set timeout of 0 milliseconds ran last???!!

OK, seems like a lot is happening here.

The first thing we need to know is JavaScript is single-threaded, meaning code will be executed line by line, sequentially, not in parallel. Operations are added to a “call stack” that follows a last-in, first-out principle.

If there is a loop that never ends, sitting on the top of the call stack, the call stack will never be able to process anything else until that loop ends. This is a huge problem and will crash your app.

So how do you take care of things that will take a while to run, but should return something eventually?

Examples of code that is not meant to run right away.

  • setTimeout(), setInterval()
  • Promises / Await
  • API calls

If asynchronous code exists, meaning if something will take a while to process, we should explicitly tell the code that, so that it gets handled outside the call stack, and is popped into the call stack later once it’s ready.

To do this we use words and syntax like await, or Promise, or setTimeout(). Whenever we see the keywords setTimeout or await we are telling JavaScript this code is not going into the call stack right now. Instead it will be handled by the browser.

Alright.. alright, so with that said, all of our other asynchronous code runs first.

So where do these deferred operations go? Well, they are actually offshored temporarily to the browser or node. The browser will handle any timer events, and HTTP requests. Afterwards, once the timer is complete or the HTTP request is completed, they are sent to the event queue that waits for the call stack to be empty before adding new things to it.

So let’s say we have some items in the event queue, this follows a first-in, first-out principle. The event loop consistently checks the call stack, and when the call stack is empty (all synchronous code is done), it adds completed asynchronous code to the top of the call stack, one at a time.

Completed asynchronous code happens for example when the timer finishes, or when an API returns data. As soon as this happens, it is ready to be sent to the call stack from the event queue.

So this explains why the console log at the end of the script was run first.

at the very end 1await / async 3 ​​​​set timeout 3

But it does not explain why the await code ran before the setTimeout…. even though the setTimeout had 0 milliseconds!

Ok… so it turns out, setTimeout belongs to the browser’s timer functionality and it’s not as important as await / async. As such, the event loop checks for those first, and pops them onto the call stack in priority. This is because micro task queue (promises, api calls) is prioritized over macro task queue (browser set timeout)

Important Note: if we had milliseconds specified for setTimeout, it does not mean that the time specified will be absolutely respected. If the call stack is occupied with other things, it will take longer. Consider it as the minimum time, before executing.

You’ll notice that although async code gets run later, they’ll still have access to the variable, num, in this example. However, it will be a reference to that variable, so if it is updated anytime before the function is popped onto the call stack, it will use the most recent value.

OK, that’s it for now! ;)

Recap:

  • Synchronous code always runs first. Any other code that is deferred, using setTimeout and await will be run once all the synchronous code is done executing
  • Once the call stack is empty, await async are prioritized in the event queue and will be executed, then any set timeouts will be executed

--

--

Jumanah Al Asadi
Jumanah Al Asadi

Written by Jumanah Al Asadi

I am just a web developer who loves writing, teaching and music :) My teaching style is playful and fun. At least I hope, lol :p

No responses yet