Be the Master of the Event Loop in JavaScript (Part 1)

Macrotasks, microtasks, execution contexts, event queues, and rAF

Moon
Moon
Dec 22, 2019 · 15 min read
Image for post
Image for post
Photo by Mark Williams on Unsplash

Guess the result of the code below:

I’ll give you some selections.

  1. start, foo, bar, end, rep, foo, baz, liz

If you don’t know the answer or your answer is between 1 and 4, you’ve come to the right post. That was quite a tricky quiz. Unfortunately, none of the selections above can be considered the answer to the quiz.

In this post, I will talk about this content below. This post may be really long as it should contain so much stuff but it is very important.

But I bet you’ll understand the workflow of the event loop in JavaScript after reading my post!

  • Execution context stack.

Related Content

You might be interested in my other posts that are related to some concepts that will be mentioned in this post.

I recommend you read my posts or MDN documentation to get a better understanding of how the JavaScript event works.

Disclaimer

Attention, folks! This post only talks about browser JavaScript, not server-side JavaScript, Node.js!

I don’t know much about Node, so it may be different from browser JavaScript. I might use some examples for a better understanding of the event loop but the example might not be exactly the same as how JavaScript works.

Some stories might be quite different from the real JavaScript model.

Execution Context Stack

Once the JavaScript runtime engine runs, the first thing it does is create an execution of a global area, which we call window.

JavaScript functions’ execution is different from their declaration. They aren’t executed unless they are IIFEs. But once they are invoked, a new area is created for the region of the function, which is called a function execution context.

There’s a function, cal, and once it’s called, JavaScript’s workflow can be represented as follows.

Image for post
Image for post
The basic concept of an execution context

The stack that manages execution contexts is called the execution context call stack (or execution context stack).

Yet, the stack size is not infinite, so if too many contexts are stacked in the stack, it’ll overflow and you’ll see this message:

Image for post
Image for post
Call stack error

In the example, a calls itself recursively and continuously until somebody (JavaScript engine) stops it.

Every function and variable has to be searchable within the topmost scope of the global execution context.

So, make sure that every function you’ve called so far in your project exists in the scopes. This concept will be mentioned later in this post or the next post of this series.

How JavaScript Works With Tasks — Task Queue

Unlike a stack that works as LIFO, every JavaScript task or job, or whatever you call, is stacked into a queue that works as FIFO. This queue is called a task queue.

Actually, JavaScript specification says task queues are sets, not queues.

Because JavaScript doesn’t remove the task that is run from the task list. But in this post, I’ll call that list queue since not only is it out of the range of this topic but also you wouldn’t have any problem understanding the concept of the event loop.

If you want to get more detail about task queue, visit this site.

But which data structure JavaScript uses isn’t what I wanted to point out. The primary thing to know is that only one task can be run at a time, no matter what. Let’s take a look at a simple example.

console.log(1);
while (true);
console.log(2);

2 will never be printed because it’s blocked by while (true). What does this basically mean?

1 will be printed at first, then JavaScript runs the second statement, while. It pushes a task to the queue and the task is executed.

But the condition of while is true, so JavaScript runs while once again and it pushes another task to the queue and the task is executed.

But the condition of while is true, so JavaScript runs while once, once again, and it pushes another task to the …

Image for post
Image for post
When console.log(1) is stored in the task queue

The figure above represents how console.log(1) was pushed into the task queue and was removed. Once JavaScript finds a statement to execute, it pushes the task into the task queue and runs it.

Once the task is executed, JavaScript gets the task out of the queue and then goes down to the next statement.

Image for post
Image for post
JavaScript stops the whole program once it’s blocked by the same task for a long time

This happens continuously because the condition of the loop is always true.

So, console.log(2) can’t even get a chance to go into the task queue. If JavaScript can’t execute other codes for that long, it shows you a warning message to exit the infinite loop.

Image for post
Image for post
JavaScript throws a message if it takes a too long time — StackOverflow

The terminology for task queue may be different in the documentation. Some use message queue.

SetTimeout — Macrotask

Let’s talk about setTimeout. Try to guess the result of this code.

console.log(1);
setTimeout(() => console.log(2), 100);
console.log(3);
setTimeout(() => console.log(4), 10);

So, I believe you already know that setTimeout runs the codes later when all of the other synchronous codes are executed.

You should first understand how JavaScript runs timers. JavaScript itself doesn’t have all of the API methods that you’ve used.

For example, in a Chrome browser, the way you could use document with JavaScript was because the Chrome engine puts the computer language, JavaScript, together with the other API modules that are like third-party libraries.

And document is a part of the modules in theDOM module, which is one of the APIs called Web APIs.

setTimeout is not a pure method of ECMAScript. It belongs to the window API that is also one of the APIs that is not in ECMAScript. But if you run setTimeout in your code, it’s executable.

It’s because the core engine under the hood mixes JavaScript up with all of the other APIs needed in a browser circumstance.

Once setTimeout is called, however, JavaScript sends a request (This isn’t a network request!) to the API that has setTimeout and moves onto the next code line.

Image for post
Image for post
Photo by Ibrahim Boran on Unsplash

OK. For a better understanding of everything, I will tell an example story.

Here’s a quite fancy hotel restaurant. There are quite a lot of tables empty at the moment.

Image for post
Image for post
Image for post
Image for post
left — Photo by Austin Distel on Unsplash / right — Photo by Kate Townsend on Unsplash

And two workers work in the restaurant. The manager, the person on the left, who arranges the waiting lines and lets customers come in or manages the reservation of the customers.

And the waiter, the person on the right, who takes an order and cleans the table.

Every customer has to wait in the waiting line until the manager says: “OK, now we have a table for you, come on in.”

Image for post
Image for post

There are two customers in the line, the star and the polygon. The manager says that the star can come inside but the polygon should wait for a bit more. And what the waiter does inside the restaurant is now to serve the food to the customers.

In this example, the manager is the task queue in JavaScript. The task queue keeps the tasks in it like the manager of the restaurant holds the customers in the line.

And the waiter is the JavaScript engine. It gives a turn/control for the action to the execution context like the waiter serves the food to the customers.

Let’s say the customer, the star shape, in the waiting line at the moment is console.log(1). And it’s enqueued. (The customer’s await in the waiting line).

Once the JavaScript engine finishes executing the current task, it takes another task if there is one in the task queue and executes it as well, like the manager lets the customer in who’s been in the front of the waiting line and the waiter takes an order and gives them food.

The next customer, the polygon shape, is console.log(3) and it can come inside once the first customer finishes eating their food, which means it’s finished running so 1 has been printed in the browser console.

Using the same process, 3 will also be printed.

Now, let’s say there were actually other customers as well but they didn’t make any reservation.

Image for post
Image for post

Like this.

The pink customers didn’t book at all. But the yellow customers booked. Then, what should you do if you were the manager of the place?

Of course, you should let customers who booked come inside only. Like this, the manager apologizes and makes them wait in another line for “non-booking customers”.

Image for post
Image for post

It seems quite a rational system. But, actually, those pink customers don’t speak English at all.

So, the manager had to hire an agency whose many translators can speak various languages and asked them to translate his words to the pink ones so they can go and wait in the right line.

So the right figure would be as follows.

Image for post
Image for post

The manager told the agency in which line the pink customers should’ve waited for their turn.

And the agency got the request but, additionally, the agency didn’t let the pinks go right way until the right time, because they didn’t want them to make trouble when they’re outside when it wasn’t the best timing to go to the waiting line.

The pink customers are setTimeout s. When JavaScript reads setTimeout, it puts it away and enqueues it into another queue, like the manager made the pink customers wait in another waiting line for those who didn’t make a reservation.

And JavaScript executes other codes like console.log before it invokes setTimeout.

I told you setTimeout isn’t included in ECMAScript. It belongs to window. If JavaScript sees setTimeout, it sends a request to the right API to deal with the invocation of setTimeout, in this case.

Then, its API module keeps it and pushes it into the different queue for non-booked customers in x milliseconds, which is the second parameter of setTimeout.

setTimeout(() => console.log(2), 100);
setTimeout(() => console.log(4), 10);

The first setTimeout is sent to the API before the second one. But the second one is added to the queue before the first one because of its time for delay.

The API module for setTimeout pushes the second one in 10ms. Then, the queue for setTimeouts would look like this.

------------------------
log(4) | log(2) |
------------------------

This new task queue is called a macrotask queue. The macrotask queue works the same as the task queue. But the difference between them is that the task queue is for synchronous statements and the macrotask queue is for asynchronous statements.

Now you can guess the right answer of this quiz.

console.log(1);
setTimeout(() => console.log(2), 100);
console.log(3);
setTimeout(() => console.log(4), 10);
// 1 -> 3 -> 4 -> 2

RequestAnimationFrame — JavaScript Rendering Steps

So far, you’ve come to the part of how JavaScript deals with setTimeout as an asynchronous function.

Many of you, however, may know that now it’s mostly better to use rAF, requestAnimationFrame, rather than to use setTimeout for many reasons.

Don’t worry, I will also talk about this shortly here, but you can read my other post about timers in JavaScript.

rAF also does the same thing as setTimeout. It isn’t executed when it’s called. But don’t you wonder which is run earlier, setTimeout or rAF?

console.log(1);
setTimeout(() => console.log(2), 0);
requestAnimationFrame(() => console.log(3));
Image for post
Image for post

Let’s go back to the restaurant again. Now we have two more customers who also didn’t book. So, the manager sends them to the mighty agency that takes care of them.

The agency tells them when to go into the line like a while ago. But soon, the agency realized that those customers, pink and blue, have a different membership of the restaurant.

The membership class of blue customers is Diamond, which is higher than Platinum which is what the pink customers' membership class is. So, the agency asks the manager what should they do with these different memberships, and the manager said: “Don’t worry about it, I’ll handle it.”

In the meantime, our loyal worker, the waiter, has been working so hard at his job.

Image for post
Image for post

The poor waiter never gets even a short break but he’s happy! What he does is serve the food to the customers and clean up the tables.

Serving the food is executing a statement. Cleaning the tables is re-rendering the browser. The JavaScript engine, the waiter in this restaurant example, takes a task from the task queue and executes it.

It keeps executing the tasks until it’s time to render the page again. It’s like the waiter serves food to as many as possible at once until it’s better to clean up the tables for the next customers.

And normally, JavaScript renders the page every 16.6ms. Then, it approximately renders the page 60 times a minute at the 16.6ms speed. This is where 60FPS comes in.

Once the waiter, back to the restaurant again, thinks it’s time to clean the tables, he tells the manager to check if there are some customers they should let in first, before he cleans up the mess on the tables.

If there are Diamond class customers, they enter and wait for him to clean the table up.

Image for post
Image for post

rAFs are executed before the page is rendered by the JavaScript engine and setTimeouts are executed after the rendering process like Diamond class customers could enter the restaurant before the waiter cleans up the tables and after the table is wiped up, Platinum class customers could come in.

rAF is also a macrotask since it shares the same task queue with setTimeout. And rAF can ensure to run before the script is parsed and the page is repainted, which means it’s executed earlier than setTimeout.

Microtask — Promise, queueMicrotask

We didn’t talk about Promise yet, that also was in the quiz at the beginning of this post.

Come on, this will be the last time we go back to the restaurant story.

The manager and the waiter have been getting used to this busy business. They’ve got so many different customers — customers who booked (normal tasks), customers who didn’t book but they were actually premium members (macrotasks).

Some of them were Platinum class (setTimeout), and some were Diamond class (requestAnimationFrame). But anyhow, they’ve been getting used to dealing with this.

But there was something they forgot. They realized that there were some customers who didn’t get their food since the cook forgot to cook their food.

So, they got so mad at them. The manager had to give them a special coupon that allows them to enter the restaurant once all of the booked customers went inside.

Image for post
Image for post

The green triangle customer was the one who didn’t get their food the other day. So, the manager “promised” them that he’d let them order after the booked customers ordered, before the customers who have a better membership grade but didn’t book.

Did you get the hint? Yes, right. The green customer is Promise. Once JavaScript reads Promises, it pushes them to another queue that isn’t the task queue or the macrotask queue.

It’s called a microtask queue. Note that the spelling of macrotask and microtask are very similar but they are very different.

The microtasks are executed only once the task queue is empty, like the green customers could enter the restaurant only after all of the booked customers (yellow) finished ordering their food.

But, for sure, the microtasks have a higher priority than the macrotasks.

So, what does this mean exactly?

console.log(1);
Promise.resolve().then(() => console.log(2));
setTimeout(() => console.log(3), 100);
console.log(4);
// 1 -> 4 -> 2 -> 3

console.log(1) and console.log(4) will be printed first since they’re just tasks. JavaScript has already read the Promise code at the line 2 but it doesn’t execute it right away.

Instead, it pushes it into the microtask queue that waits for when all of the tasks are dequeued out of the task queue. And when JavaScript reads setTimeout, it sends it to the web API.

The web API module for setTimeout sends it to the macrotask queue in 100ms. When the macrotasks in the macrotask queue are executed, all of the tasks are executed and all of the microtasks are executed.

But unlike requestAnimationFrame, setTimeout doesn’t care about rendering steps. They’re just sent to the macrotask queue after a number of milliseconds and wait for their turn.

Once it’s their turn, they’re executed. On the other hand, rAF doesn’t take a millisecond as an argument. Instead, if rAF is at the right timing to be executed, JavaScript keeps it in the queue and runs it right before the page is rendered.

In other words, if microtask calls another microtask, which calls another microtask, which calls another microtask… if this happens infinitely, the browser will never be able to be rendered. Because the render steps are after the invocation of microtasks.

If you want to push a task into the microtask but your code isn’t Promise, you can use queueMicrotask. This method adds your function to the microtask queue. But this isn’t supported in IE-family browsers at all.

Summary

Finally. You’ve done so well to read all of the complex concepts. I will briefly summarize what I have talked about in this post.

A tasks queue is a queue in which JavaScript tasks are stored. It could be changing the color of the DOM element, or increasing the count of a value. The JavaScript tasks have the highest priority for execution.

A macrotask queue is a queue for setTimeout and requestAnimationFrame. Actually, there are more tasks considered a macrotask queue, please check out the link I’ve attached in the resources section.

setTimeout and requestAnimationFrame are asynchronous functions. And they are a part of the window web API module. So, JavaScript sends them to the API module, and the module pushes them to the macrotask queue at the right time.

setTimeout, however, could bother the render frame intervals, which are 16.6ms, since it’s executed after the rendering. If the callback function in setTimeout takes forever to be done, the next tick for the rendering will also be delayed in execution.

This is where requestAnimationFrame comes in. rAF is executed before rendering.

A microtask queue is a queue for Promise — of course, there are more than just Promise.

One thing to remember is that microtasks are only executed once the task queue is completely empty. If the task queue isn’t empty, none of the microtasks are executed. And the microtasks are executed before the macrotasks are executed.

Back to the Quiz

Now I believe you can guess the right answer to this quiz!

The fun result is that the answers are slightly different in different browsers.

Image for post
Image for post

Safari prints the words in a different order, it’s because Safari runs rAF after the rendering.

Conclusion

Thank you so much for reading my post! I took so much time for me to grasp the whole concept of the JavaScript event loop. I’ve read the same articles over and over again and watched the videos a few times.

I wanted to write something for beginners like me a few days ago to give them another study reference they could use for study. Thank you for reading my post again!

Better Programming

Advice for programmers.

Sign up for The Best of Better Programming

By Better Programming

A weekly newsletter sent every Friday with the best articles we published that week. Code tutorials, advice, career opportunities, and more! Take a look

By signing up, you will create a Medium account if you don’t already have one. Review our Privacy Policy for more information about our privacy practices.

Check your inbox
Medium sent you an email at to complete your subscription.

Thanks to Zack Shapiro

Moon

Written by

Moon

Frontend React w/ Typescript developer based in S.Korea. Interested in UX/Testing/FE. Currently working at Watcha. mgyang95@gmail.com

Better Programming

Advice for programmers.

Moon

Written by

Moon

Frontend React w/ Typescript developer based in S.Korea. Interested in UX/Testing/FE. Currently working at Watcha. mgyang95@gmail.com

Better Programming

Advice for programmers.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store