JavaScript: Micro vs Macro Tasks Exercises and Sources to Learn ๐Ÿ’†

JavaScript Micro Task and Macro Task exercises and some sources to learn them.

ยท

8 min read

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:

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
The strings will be logged in this order: "Task 2", "Task 3", "Task 5", and "Task 1".

So this is what's happening:

  1. 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).

  2. 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.

  3. 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.

  4. On the fourth line, the engine encounters console.log("Task 5"), which gets added to the call stack, executed, and popped out.

  5. 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
The strings will be logged in this order: "Task 2", "Task 3", "Task 5", "Task 4", and "Task 1".

So this is what's happening:

  1. 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.

  2. 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.

  3. 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).

  4. On the seventh line, the engine encounters console.log("Task 5"), which gets added to the call stack, executed, and popped out.

  5. 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 that console.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
"Task 5", "Task 3", "Task 1", "Task 6", "Task 2", and "Task 4"

So here's what's happening:

  1. The first execution will be on line 15, therefore "Task 5" is displayed.

  2. On line 16, random2() gets executed. Inside the random2, console.log("Task 3") will be executed first. After that, random2 calls for random1 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".

  3. But here's a twist, with the await keyword, the rest of the random2 function gets suspended. The engine then jumped out of the random2 function and continued to execute the rest of the codes inside the execution context the random2 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 be console.log("Task 6"), which then displays "Task 6".

  4. 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
"Task 5", "Task 2", "Task 6", "Task 3", "Task 1", and "Task 4".

Okay, here's what's happening:

  1. Similar to before, the first execution will be on line 15, therefore "Task 5" is displayed.

  2. Then on line 16, random2 gets called. It will first execute the console.log("Task 2"), which will display "Task 2".

  3. 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.

  4. After that, random2 calls the random1 function, which returns a promise. The callback inside the promise constructor gets executed, which then adds the setTimeout method to the call stack. The callback inside the setTimeout is moved to the Web API, which is then also enqueued to the Macro Task queue.

  5. Since there's an await keyword, the engine will jump out of the random2 function and continue the rest of the codes in the Global Execution Context. In this case, it will be console.log("Task 6") which then displays "Task 6".

  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.

  7. 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".

  8. 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.

  9. 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.

  10. 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 the await 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
"Task 5", "Task 2", "Task 6", "Task 1", "Task 4", and "Task 3".

Phew..., that is all for this one. To close things out, I want to quote that:

Before getting farther into this, it's important to note again that most developers won't use microtasks much, if at all. - MDN

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 ๐Ÿป!

ย