DEV Community

Brendon O'Neill
Brendon O'Neill

Posted on

Let's understand the Event Loop

Event Loop

The event loop is JavaScript's mechanism for handling asynchronous operations. It continuously checks if the call stack is empty and, if so, moves the next task to the stack for execution. This allows non-blocking operations, keeping the application responsive.

Let's look at this example below. What do you think will be the printout of the number from 1 to 7?

console.log('1');

setTimeout(() => {
  console.log('2');
}, 0);

Promise.resolve(console.log('3')).then(() => {
  console.log('4');
  Promise.resolve().then(() => {
    console.log('5');
  });
});

queueMicrotask(() => {
  console.log('6');
});

console.log('7');


Enter fullscreen mode Exit fullscreen mode

If you said :

// Console
1
3
7
4
6
5
2
Enter fullscreen mode Exit fullscreen mode

Well done, but if not, don't worry, we will discuss why and try to break it down and understand how the event loop works.

First, we will start with the call stack. When JavaScript is run, it is called synchronously as JavaScript is single-threaded. It will call each piece of code line by line, starting with the console.log('1'). As console.log is a synchronous global function, it will be placed on the call stack called, executed and removed from the call stack.

console.log added call stack

// Console
1
Enter fullscreen mode Exit fullscreen mode

console.log removed call stack

Call Stack

The call stack is used to call tasks as JavaScript goes line by line through its code.

Let's look at this example for another look at how the call stack works:

function taskOne()
{
  console.log("1");
  taskTwo();
}

function taskTwo()
{
  console.log("2");
}

function taskThree()
{
  console.log("3")
  taskTwo();
  taskFour();
}

function taskFour()
{
  console.log("4");
}

taskOne();

Enter fullscreen mode Exit fullscreen mode

JavaScript goes through this code line by line until it gets to taskOne(), which is when it adds the first task to the call stack.

taskOne added to call stack

JavaScript will then go to the first line of code in the taskOne function and add the console.log('1') to the call stack. It will run the code and then remove it from the call stack.

(Added to the call stack)

console.log added to call stack

// Console
1
Enter fullscreen mode Exit fullscreen mode

(Removed from the call stack)
console.log removed to call stack

Next, JavaScript will call the taskTwo function and add that to the call stack and will go line by line into that function.

taskTwo added to call stack

JavaScript will now call the console.log('2') and add it to the call stack. It will run the code and then remove it from the call stack just like console.log('1').

(Added to the call stack)

// Console
1
2
Enter fullscreen mode Exit fullscreen mode

(Removed from the call stack)
taskTwo added to call stack

Now that all the lines of code have been run in taskTwo, this function will be removed from the call stack.

console.log removed to call stack

This same process will be done for taskThree and taskFour where JavaScript will run each line of code, adding and removing from the call stack as it executes. The call stack uses a data structure called a stack, which can only add to the top of a stack and can only remove from the top, also known as last in, first out. I won't go into detail on how stacks work as that's a whole other blog post in itself.

Two examples I can give you are first, think of it like a stack of books as you stack book on book, the tower of books gets larger. If you try to take a book from the bottom of the tower, the whole tower could fall, but if you take the top book each time, you can dismantle the tower without it collapsing.

Second, think of an array that you can only add and remove the first element. You would use the shift and unshift functions to add and remove from the array as you execute your JavaScript code.

let arr = [];

arr.unshift(1) // arr = [1]
arr.unshift(2) // arr = [2,1]
let task = arr.shift() // arr = [1]
arr.unshift(3) // arr = [3,1]
Enter fullscreen mode Exit fullscreen mode

This is how the call stack works when JavaScript code is running. So let's go back to the original question.

console.log('1');

setTimeout(() => {
  console.log('2');
}, 0);

Promise.resolve(console.log('3')).then(() => {
  console.log('4');
  Promise.resolve().then(() => {
    console.log('5');
  });
});

queueMicrotask(() => {
  console.log('6');
});

console.log('7');


Enter fullscreen mode Exit fullscreen mode

The JavaScript will go down this code line by line executing each piece of code, starting with the console.log('1'), it will add it to the call stack, run it and then remove it from the call stack. Next, it will come to the setTimeout function, which brings up a new section of the event loop, the queues.

Macrotask & Microtask Queues

There are two types of queues in the event loop the Macrotask queue, also known as the Task queue and the Microtask queue. Each queue handles different tasks, the Macrotask handles callback functions like setTimeout and setInterval. There is also the requestAnimationFrame, Event Handlers, fetching data and Web APIs. The Microtask queue handles tasks like Promises and queueMicrotasks.

How these queues work is that when JavaScript needs to run an asynchronous piece of code like the setTimeout function, it first adds it to the call stack, then removes it and calls the callback or web API. As it is the setTimeout function, it waits for some milliseconds to pass before adding it to the Macrotask queue. This task will not run until JavaScript goes line by line adding and removing from the call stack. Once the call stack is empty, it will call the queues to run their tasks, but there are some rules the queues must follow.

The Microtask queue will always be run first, as they are considered more important. Each task will be run one by one on the call stack until the call stack is empty. When the Microtask queue is complete, the Macrotask can start to run tasks one by one, but before each task is called, it must check if the Microtask queue is empty. Like the Microtask queue, each task will be run one by one on the call stack until the call stack is empty.


setTimeout(() => {
  console.log('1');
}, 0);

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

Enter fullscreen mode Exit fullscreen mode

The setTimeout callback is added to the call stack, then removed and placed to the side until it counts the milliseconds until execution or for visual purposes in the Web API section.
setTimeout

setTimeout

Once the time is complete, the task is placed in the Macrotask queue waiting for the call stack to be empty. As this is happening the call stack can continue executing code and gets to the Promise.resolve. Because Promise.resolve is synchronous, if a task were in the resolve function, it would be called immediately on the call stack.

For the .then as it is a promise it will be pushed to the Microtask queue to wait for the call stack.

Just like how we discussed, when the call stack is empty, the Microtask queue will run its tasks one by one. Then the Macrotask queue will run its tasks.

Just like the call stack, these queues that handle the micro and macro tasks are another data structure, but this time, elements are added to the back and removed from the front, also known as first in, first out.

An example to explain the queue system is like in a theme park, think of the ride as the call stack. When the ride is empty, people start to get on the ride, but the people with the express tickets get to go on first, just like the Microtask queue. Once the Express queue is empty, people with normal tickets can get on the remaining seats. Then as the ride becomes free again, they first check the express queue before letting people in from the normal queue. This is a similar system to how the event loop chooses the next task to place on the call stack once it is empty.

Now that we've seen how it works let's go over the first question and explain how we got.

// Console
1
3
7
4
6
5
2
Enter fullscreen mode Exit fullscreen mode
console.log('1');

setTimeout(() => {
  console.log('2');
}, 0);

Promise.resolve(console.log('3')).then(() => {
  console.log('4');
  Promise.resolve().then(() => {
    console.log('5');
  });
});

queueMicrotask(() => {
  console.log('6');
});

console.log('7');

Enter fullscreen mode Exit fullscreen mode

We have done the first console.log()

// Console
1
Enter fullscreen mode Exit fullscreen mode

Next we move onto the setTimeout that will get put on the call stack, then removed and placed to the side until its timer completes, to then be put into the Macrotask queue.

The next bit is a lot, but we take it apart and explain what happens. As Promise.resolve is synchronous, it will call the task within its parameters.

// Console
1
3
Enter fullscreen mode Exit fullscreen mode

The .then is a promise, so it will be added to the Microtask queue and we will deal with the rest of it when we complete the call stack.

After the Promise is a queueMicrotask. You can think of this as a task that gets pushed straight to the Microtask queue after the promise.

The last piece of code is the console.log so we add that to the call stack, then print out the value and remove it, emptying the call stack.

// Console
1
3
7
Enter fullscreen mode Exit fullscreen mode

The call stack is now empty, so we go to the Microtask queue and remove the .then() and add it to the call stack. First, we print out the value of the console.log() and if the Promise.resolve had a task, it would run that, but because it is empty it just adds the new .then() to the Microtask queue after the queueMicrotask.

// Console
1
3
7
4
Enter fullscreen mode Exit fullscreen mode

The queueMicrotask is removed from the Microtask queue and added to the call stack and prints out the console.log and is removed from the call stack.

// Console
1
3
7
4
6
Enter fullscreen mode Exit fullscreen mode

We then return to the Microtask queue for its last task and add it to the call stack, print out the console.log and remove it from the call stack.

// Console
1
3
7
4
6
5
Enter fullscreen mode Exit fullscreen mode

The final task is to check the MacroTask for the final task. We come to the answer to the first question.

// Console
1
3
7
4
6
5
2
Enter fullscreen mode Exit fullscreen mode

Conclusion

The Event Loop is a confusing concept, but once you understand it, you can debug your code with more confidence knowing how the system works and why some tasks get run before others.

Bonus Resources

These are some resources that helped me understand the event loop and how it works:

Videos:
JavaScript Visualized - Event Loop, Web APIs, (Micro)task Queue by Lydia Hallie

What the heck is the event loop anyway? | Philip Roberts | JSConf EU by JSConf

Website Playground:
JavaScript Visualizer 9000

Top comments (0)