Let’s look into this example:

JavaScript

        setTimeout(() => { console.log(‘first’); }, 0);
console.log(‘second’);
      

What do you expect to see in the console? It seems like because we’re waiting for 0 milliseconds we should see "first" before "second", but in reality, you get "second" before "first". If you have troubles in explaining why does it happen – dive with me deep to the underground world of a JavaScript engine.

Call Stack

As you have probably heard, JavaScript has a single Call Stack, which is needed to keep track of functions we’re currently executing and to know what will be executed next. A “stack” is an array-like data structure, which is similar to a pile of plates: you can only put them on top of each other, and in any time you can remove only the top one.

When you want to execute a function, it’s added to the top of the Call Stack. Then if that function calls another function, the other function is placed on top of the first function. If you remember how does look like a stack trace when you get an error in the console – that’s exactly how did the Call Stack look in the time when the error happened.

JavaScript engine fills the Call Stack with function calls in the reverse order: the first called function will be at the very bottom of the Stack, and the last function – at the very top.

JavaScript engine fills the Call Stack with function calls in the reverse order: the first called function will be at the very bottom of the Stack, and the last function – at the very top.

by Dmitry Salnikov

But what happens if you call setTimeout() or make a request? We can’t execute the next function from the Call Stack until the previous function hasn’t finished yet. This should freeze the browser while our request is in flight, but we know it doesn’t happen. And the reason – Event Table and Event Queue.

Event Table and Event Queue

When you call an asynchronous function like setTimeout(), it’s added to the Event Table. Think about it as a table-like data structure, which keeps track of what function should be triggered after a certain event. It doesn’t execute the function or add to the Call Stack once the event (touchstart, timeout, click) happens. It only sends the function to the Event Queue.

Schematic view of the Event Table and Event Queue. On the left side we have two scheduled events: click event and a timeout event (set via setTimeout()) and assigned callback functions. The Event Queue is empty. Then, let’s imagine a user clicks the button and then 5s timeout passes. On the right side, the Event Table moves callbacks to the Event Queue in the same order events happened.

Schematic view of the Event Table and Event Queue. On the left side we have two scheduled events: click event and a timeout event (set via setTimeout()) and assigned callback functions. The Event Queue is empty. Then, let’s imagine a user clicks the button and then 5s timeout passes. On the right side, the Event Table moves callbacks to the Event Queue in the same order events happened.

by Dmitry Salnikov

Event Queue – is a queue data structure, where you can add an item to the back and remove an item only from the front. All it does – helps keep the order of callbacks in which they should be executed. Again, it doesn’t execute functions or add them to the Call Stack on its own, only stores them in order. And here comes the Event Loop.

Event Loop

Now you know enough to understand what is the Event Loop. It’s a constantly running process (loop) that checks if the Call Stack is empty. If it’s not empty – Event Loop executes the top task in the Call Stack, and if it is empty – looks into Event Queue and moves callbacks from there to the Call Stack. If the Call Stack and Event Queue are both empty – Event Loop does nothing. This is called a “tick”. On each tick this sequence repeats.

Each “thread” in JS gets its own Event Loop (each web worker gets its own EL), so it can execute independently. All browser windows on the same origin share the same Event Loop so that they could synchronously communicate.

Remember our puzzle with logs?

JavaScript

        setTimeout(() => { console.log(‘first’); }, 0);
console.log(‘second’);
      

Now I’m sure you can explain why “second” is printed before “first”. When JavaScript sees a setTimeout() it adds your callback to the Event Table. Then JS engine immediately continues executing and prints “second” to the console. Now when the Call Stack is empty (we have no code to run), Event Loop checks Event Queue and finds there your timeout callback, moves it to the Call Stack and then executes it in the next iteration.

Event Loop is a constantly running process that checks if the Call Stack is empty. If it’s not empty –  Event Loop executes the top task in the Call Stack, and if it is empty – looks into Event Queue and moves callbacks from there to the Call Stack. Meanwhile, the Event Table pushes new tasks to the Event Queue, when events it is tracking are happening.

Event Loop is a constantly running process that checks if the Call Stack is empty. If it’s not empty – Event Loop executes the top task in the Call Stack, and if it is empty – looks into Event Queue and moves callbacks from there to the Call Stack. Meanwhile, the Event Table pushes new tasks to the Event Queue, when events it is tracking are happening.

by Dmitry Salnikov

There is another good example of how useful can be the knowledge of internal JavaScript mechanisms: imagine you need to do a big number of recursive calls, but you get a “Stack overflow” error message because there is a limit of how big can be the Call Stack. What you can do – wrap your recursive call into setTimeout(recursiveFunction(), 0). Thus you move all the recursive calls to the Event Table and avoid directly piling up the Call Stack. It’s better to restrain from these hacks, but it’s a good demonstration of how JS engine works.

Macro- and microtasks 

For each loop of the Event Loop, one task is completed out of the Call Stack. But it turns out that not all tasks are equal. Some are considered as macrotasks and some – as microtasks. And – surprise! – they occupy two different stacks and never mix.

Event Loop first visits the main stack with macrotasks, executes one function from the top and then visits the microtasks queue and executes all functions from there before moving on. The Call Stack – is the macrotasks Stack, and since we are executing the entire microtasks queue on each macrostask, we can say that every macrotask has its own microtasks queue.

Examples of macro: I/O tasks, setTimeout, setInterval, setImmediate.

Examples of micro: promises, process.nextTick.

Wait, what?... Promises are considered as microtasks? Yes, because if they were macro – then a chain of promises would require multiple loops of the Event Loop to complete – not the best variant for promises.

Microtasks are usually scheduled for things that should happen straight after the currently executing script, such as reacting to a batch of actions, or to make something async without taking the penalty of a whole new task.

Now look at this code and try to predict the result in the console:

JavaScript

        console.log('start');

setTimeout(() => {
  console.log(‘timeout’);
}, 0);

Promise.resolve()
  .then(() => {
    console.log('promise 1');
  })
  .then(() => {
    console.log('promise 2');
  });

console.log('end’);
      

The correct answer here is: “start”, “end”, “promise 1”, “promise 2”, “timeout”. Confused? Let’s go step by step to understand why:

1. console.log('start') prints “start” to the console.

2. setTimeout() schedules an event in the Event Table. Event Table keeps track of this event and once it happens (in 0 ms, that is immediate) it puts the callback to the Event Queue.

3. Promise.resolve() settles a promise and all subsequent then() callbacks are treated as microtasks, so we add them both to the microstasks queue for the currently running task.

4. console.log('end’) prints “end” to the console.

5. We finished all macrostasks from the Call Stack and it’s time to go visit the microtasks queue.

6. () => { console.log('promise 1'); } prints “promise 1” to the console.

7. () => { console.log('promise 2'); } prints “promise 2” to the console.

8. We finished the entire microtasks queue. It’s time to go check the Event Queue.

9. In the Event Queue we find the timeout callback and move it to the top of the Call Stack.

10. The “tick” is over.

11. In the next iteration of the Event Loop, we again take the task from the top of the Call Stack, and it’s () => { console.log(‘timeout’); } which prints “timeout” to the console.

12. The macrotasks are completed, let’s check the microtasks queue. It’s empty? Perfect! Not let’s check the Event Queue. Also empty.

13. The program is done!

This can be a new mind-blowing concept for you, if you’ve just learned about all of these first time. But congratulations, you’ve survived and know enough to build your own JavaScript engine. Or at least now you have something to talk about during a coffee break with collegues.

Useful materials: