Photo by Hal Gatewood on Unsplash
JavaScript: Micro vs Macro Tasks Exercises and Sources to Learn ๐
JavaScript Micro Task and Macro Task exercises and some sources to learn them.
Sources to Learn
In this article, I'm gonna provide some exercises that could help refresh your memory on the Micro vs Macro Tasks topic. If you are not very familiar with the topic, here are a few sources that I think are great for learning this topic:
[JavaScript Under The Hood [3} Asynchronous JavaScript, Task Queue & Event Loop (by Traversy Media)](youtu.be/28AXSTCpsyU?si=VSBVoPziF9v6xaKZ)
Doesn't go deep into the micro vs macro tasks topic, but explains the event loop, task queue, browser APIs, and call stack which I think is essential for micro and macro tasks.
The JavaScript Event Loop: Explained (by Ayush Verma)
A Medium article that explains Call Stack, Web APIs, Event Queue, Micro-tasks, and Macro-tasks well. It also has GIFs that help with the explanations.
Using microtasks in JavaScript with queueMicrotask() (by MDN)
Now, let's get to the exercises! ๐ฏโโ๏ธ
First exercise
setTimeout(() => console.log("Task 1"), 0)
console.log("Task 2")
new Promise(() => console.log("Task 3")).then(() => console.log("Task 4"))
console.log("Task 5")
Stop the scroll and guess in what order the strings will be logged!
Answer
So this is what's happening:
In the first line, the
setTimeout
method gets added to the call stack. Although the timer is 0 seconds, it's recognized to be native to the browser and its callback function still gets added to the Web API. The callback function is then moved to the Macro Task queue since the timer is over (setTimeout
is a Macro Task).On the second line,
console.log("Task 2")
gets added to the call stack, displays the output, and gets popped out from the call stack.On the third line, the engine encounters a promise constructor. The callback inside the constructor gets called immediately/synchronously (the callback is also called the "executor" and you can read more about it on MDN). Therefore, "Task 3" is displayed and "Task 4" is never displayed because the promise is never resolved.
On the fourth line, the engine encounters
console.log("Task 5")
, which gets added to the call stack, executed, and popped out.After the call stack is empty, the engine will check for the Micro Task queue. But since it's empty, it will then check the Macro Task queue for tasks. And yes it has one! our callback from the
setTimeout
on the first line. The callback then gets added to the call stack, which then adds the console.log, displays "Task 1", and gets popped out orderly.
Second exercise
Let's add a little twist to the first exercise
setTimeout(() => console.log("Task 1"), 0)
console.log("Task 2")
new Promise((res) => {
res()
console.log("Task 3")
}).then(() => console.log("Task 4"))
console.log("Task 5")
Guess in what order the strings will be logged!
Answer
So this is what's happening:
Similar to the previous exercise, on the first line, the
setTimeout
method gets added to the call stack. Its callback function gets added to the Web API and then moved to the Macro Task queue since the timer is over.On the second line,
console.log("Task 2")
gets added to the call stack, displays the output, and gets popped out from the call stack.Similar to the previous exercise, the callback inside the constructor gets called immediately/synchronously. Although the promise was resolved earlier than
console.log("Task 3")
, the callback will not stop executing even after the promise is resolved (I found a relevant stack overflow answer here). Therefore,console.log("Task 3")
gets executed.Now, the reason why "Task 4" is not displayed before "Task 3" is because the callback inside the "then" method is a Micro Task. This means it will be enqueued in the Micro Task queue and will be executed after all the synchronous tasks are done (the call stack is empty).
On the seventh line, the engine encounters
console.log("Task 5")
, which gets added to the call stack, executed, and popped out.After the call stack is empty, the engine will check for the Micro Task queue (Micro Task is a higher priority than Macro Task). In the Micro Task queue, we have a function that
console.log("Task 4")
, which is then executed. The engine will then continue to check for tasks in the Micro Task queue. Only after the Micro Task queue is empty, then the engine moved to the Macro Task queue. In the Macro Task queue, the engine then finds a function thatconsole.log("Task 1")
, which then gets executed.
Third exercise
We are using async
this time.
function random1() {
return new Promise(res => {
console.log("Task 1")
res("Task 2");
})
}
async function random2() {
console.log("Task 3")
const value = await random1();
console.log(value)
console.log("Task 4")
}
console.log("Task 5")
random2();
console.log("Task 6")
Guess in what order the strings will be logged!
Answer
So here's what's happening:
The first execution will be on line 15, therefore "Task 5" is displayed.
On line 16,
random2()
gets executed. Inside therandom2
,console.log("Task 3")
will be executed first. After that,random2
calls forrandom1
which returns a promise. Similar to previous exercises, the callback inside the promise constructor gets executed immediately, which displays "Task 1" before resolving with "Task 2".But here's a twist, with the
await
keyword, the rest of therandom2
function gets suspended. The engine then jumped out of therandom2
function and continued to execute the rest of the codes inside the execution context therandom2
function is called (this is also explained here). In this case, the execution context I am referring to is the Global Execution Context (here's a video to learn more about execution context).Or to put it simply, in this case, the engine will then continue to execute the codes after
random2
is called. That would beconsole.log("Task 6")
, which then displays "Task 6".After the Global Execution Context is empty, the engine will then check for the Micro Task queue for tasks. Since the promise returned by
random1
is already resolved, then it will be waiting in the Micro Task queue.random2
then gets added to the call stack again and continues where it's left off, displaying "Task 2" and then "Task 4".
Last exercise
Let's add more twists to the third exercise.
function random1() {
return new Promise(res => {
setTimeout(() => res("Task 1"), 0)
})
}
async function random2() {
console.log("Task 2")
setTimeout(() => console.log("Task 3"), 0)
const value = await random1();
console.log(value)
console.log("Task 4")
}
console.log("Task 5")
random2();
console.log("Task 6")
Guess in what order the strings will be logged!
Answer
Okay, here's what's happening:
Similar to before, the first execution will be on line 15, therefore "Task 5" is displayed.
Then on line 16,
random2
gets called. It will first execute theconsole.log("Task 2")
, which will display "Task 2".It will then call the
setTimeout
method.setTimeout
gets added to the call stack, its callback is moved to the Web API, which is then eventually moved to the Macro Task queue.After that,
random2
calls therandom1
function, which returns a promise. The callback inside the promise constructor gets executed, which then adds thesetTimeout
method to the call stack. The callback inside thesetTimeout
is moved to the Web API, which is then also enqueued to the Macro Task queue.Since there's an
await
keyword, the engine will jump out of therandom2
function and continue the rest of the codes in the Global Execution Context. In this case, it will beconsole.log("Task 6")
which then displays "Task 6".Now, the engine is all done in the Global Execution Context and the call stack is empty. The engine will then check for the Micro Task queue. But for now, it will be empty. Since the Micro Task queue is empty, it will then check the Macro Task queue.
Inside the Macro Task queue, we have 2 tasks queuing. The first one is the callback that
console.log("Task 3")
. The callback gets added to the call stack, which is then executed, and displays "Task 3".After that callback, the engine will then check the Micro Task queue again and it will be empty for now. Therefore, the engine then moves on to the Macro Task queue.
In the Macro Task queue, we only have 1 task remaining. That would be the callback function from the
setTimeout
inside the promise constructor. This callback then gets added to the call stack, and then it resolves the promise.After the call stack is empty again, it will check the Micro Task queue. This time is not empty since the promise is resolved. The
random2
gets added to the call stack again, and continue the rest of the code after being suspended by theawait
keyword, which will display "Task 1" and "Task 4".
Bonus exercise
Guess what would be displayed:
function random1() {
return new Promise(res => {
setTimeout(() => res("Task 1"), 0)
})
}
async function random2() {
console.log("Task 2")
setTimeout(() => console.log("Task 3"), 100)
const value = await random1();
console.log(value)
console.log("Task 4")
}
console.log("Task 5")
random2();
console.log("Task 6")
Answer
Phew..., that is all for this one. To close things out, I want to quote that:
Hahaha.
Thank you for reading this article. If you find any of what's written is wrong, please hit me up. I still have a lot to learn and I'm always looking forward to discussions.
Cheers ๐ป!