Macrotasks and Microtasks in JavaScript

Do you get confused between why promises are executed first rather than setTimeout or setInterval? Or you messed up an interview because of this. Let's find out why this happens

First check your understanding of microtask and macrotask using an output question. So, What would be the output of this code snippet?

If you were able to answer this correctly, Congratulations! You have a good idea about event loop and, task queues. This post is not for you.

So let’s forget about microtasks and just look at this simple code snippet-

The output of code snippet would be -

Please notice that "Good Night" will be logged after at least 5 seconds. So what happens behind the scenes?

Step by Step:

  1. Entire script is added to call stack.
  2. console.log(“Good Morning”) is executed, and then logged on console.
  3. setTimeout(() => ..) is executed and passed on to the web api.
  4. console.log(“Good Afternoon”) is executed, and then logged on console.
  5. The call stack is being checked: if it’s empty, the event loop pops and event from the callback queue (in our case the callback function) and push it to call stack.
  6. console.log(“Good Night”) is executed, and then logged on console.

Since ES6 (JavaScript Standard) was released, it turned to be a big player for async executions in JS. It provided a different way of executing async code, adding a concept called "Microtask Queue" (also called "Job Queue"). We can now call the callback queue "Macrotask Queue", and assign a new layer on top of it.
At the end of every tick of the JavaScript Event Loop, the tasks inside the microtask queue are executed. This new functionality assures us that certain async actions will be added to the microtask queue, therefore executed right after the tick, before the next task in the macro queue.

Now let’s come to queueMicrotask. This function will explicitly put a task in microtask queue. This is what happens when a promise resolves. If you chain multiple then() statements, they will all be put in the microtask queue and are guaranteed to be executed before handling control back to browser’s event loop. So if you keep queuing tasks in the microtask queue forever, your browser will become unresponsive. You can’t click a button, can’t scroll, nothing…, GIFs stop, animations stop etc. The control will never be passed to browser renderer.

Now let’s try original question again.

The output of above code snippet would be -

Step by Step Explanation:

  1. The entire script is added to the call stack.
  2. console.log(1) is executed and then logged on the console.
  3. setInterval(() => {..}, 0) is executed and passed on to the web API. The callback function would be added to the task queue after every 0 seconds.
  4. settimeout(() => ..) is executed and passed on to the web API. The callback function would be added to the task queue after 0 seconds.
  5. Promise.resolve().then(() => ..); is executed and since the promise is resolved directly, the callback would be pushed to job queue.
  6. queueMicrotask(() => …) is executed and it also adds the callback to the job queue.
  7. console.log(8) is executed and then logged on the console.
  8. Now, the job queue is checked and all the pending tasks are pushed onto the call stack one by one.
  9. () => console.log(5) is pushed onto stack and executed.
  10. () => {
    console.log(6);
    queueMicrotask(() => ..);
    } is executed logging 6 on console and adding another callback to job queue.

  11. () => console.log(7) is pushed onto stack and executed.
    Now since the job queue is empty, the task queue is checked again to see if any task is pending.

  12. So () => { console.log('interval') } is pushed onto stack and executed.

  13. queueMicrotask(() => ..) is executed adding callback function to job queue.

  14. console.log(4); is pushed onto stack and executed.

  15. () => {
    console.log(2);
    setTimeout( () => {
    console.log(3);
    clearInterval(interval);
    }, 0);
    } is executed logging 2 on console and adding another task to task queue.

  16. () => { console.log('interval') } is pushed onto stack and executed.

  17. () => {
    console.log(3);
    clearInterval(timer);
    } is executed logging 3 on console and clearing the interval.

The important points are:

  • Tasks are taken from the Task Queue and is called macrotask.
  • Microtasks are processed when the current task ends and the microtask queue is cleared before the next macrotask cycle.
  • Microtasks can enqueue other microtasks. All are executed before the next task inline.
  • UI rendering is run after all microtasks execution.

29