The JavaScript event loop: micro-tasks and macro-tasks

Siddharth Jain

Humans are incredibly multi-threaded; we may be having our breakfast while reading a newspaper. Also, we might simultaneously listen to a song. Here, a human performs three different tasks all at the same time. On the other hand, JavaScript engines can have only one main thread of execution, where a process (say function B) cannot be executed until the process (function A) is complete.

Humans do not have things that block related things. We do have exceptions and that can be when we sneeze because as soon as we sneeze, we lose the ability to read or eat, where our human body becomes entirely single-threaded. And then when it is over, we return back to the present and try to find out whether our food got spilt, or which line we were reading, or we might even wonder if we still have our pair of eyes.

In JavaScript, interaction or rendering may get blocked due to the modification of the same DOM by multiple bits of code. The code written in that way is like a sneeze. After completion of the entire process or thread, the information must be returned on to the main thread; that is where the event loop comes into play.

Event Loop

Being single-threaded, JS creates the concept of event loop to run multiple tasks asynchronously. The event loop performs the function of constant monitoring of the message queue and the execution stack. The moment the execution stack becomes empty, the event loop lines up the first callback function on the execution stack.

What are these callback functions, execution stack, web APIs, message queue?

Execution Stack

Any JavaScript code requires an environment to run in, which is known as an execution context. It is similar to a container in which variables are stored, and the evaluation and execution of our code takes place. Every time we call a function, it receives a new execution context, which is placed above the current context. This forms what we refer to as the execution stack.

Web APIs

Things like HTTP requests for AJAX, setTimeout, DOM manipulation methods, amongst others, exist outside the JavaScript engine. If we have setTimeout function of two seconds, then our timer keeps running here for two seconds asynchronously, to ensure that our code keeps running without getting blocked. As soon as the setTimeout function is called, our callback function creates the timer right inside the web APIs environment and stays there until its work is completed asynchronously. The callback function remains attached to the timer until it is finished instead of calling the callback function at that moment. This allows us to keep executing our code instead of waiting, as the timer does not stop working in the background.

When the timer disappears after its time is up (two seconds, in this scenario), our callback function shifts to the message queue until the execution stack is empty and can execute the callback function.

Message Queue

All callback functions are temporarily placed on the message queue until the execution stack is empty and they can be executed. DOM events undergo the same process, where our event listeners wait in the web APIs environment for the occurrence of a certain event. The instance the event occurs, the callback function sits ready in the message queue for execution.

How does the execution of the callback functions take place after they reach the message queue? This is where the event loop finally functions.

The whole process of reading of code, queueing up of tasks, and execution of the tasks is known as the event loop. In actual terms, it is observed that all tasks that are created are not the same. They are of two types: “macro-tasks” and “micro-tasks”, which occupy their own queues instead of mixing, inside message queue.

Micro-tasks and Macro-tasks

Tasks in message queue are not treated equally. They are further divided into micro-tasks and macro-tasks. Instead of mixing, these two types of tasks occupy different queues.

The question that arises is how the event loop would decide which queue is dequeued from and pushed to the call stack when it is empty. The solution is dependent upon the following rules:-

One macro-task is completed from the macro-task (Task) queue inside message queue. On completion of that task, the event loop goes to the micro-task (Job) queue. The “event loop” does not look into the next action until the completion of the entire micro-task (Job) queue. It finishes the entire micro-task queue and then moves back to the macro-task queue.

Task Queue  Micro-task  Render Macro-task

This is done because user actions are more important than any background task such as setTimeout task, to give a user the best user experience.

Consider the event loop algorithm:-

In JavaScript Async programming, understand the differences in execution strategy of events and promises, the event loop queues, the kind of tasks that are scheduled in Task queue and microtask queue:

1st part :- It takes one task from task queue (Macro-task)

Var task = taskQueue.getOldestTask();
execute (task);

2nd part :- After each task it has infinite loop of reading micro-task queue and execute all of them which are in queue until it is empty. If there are 1000 micro-task, event loop will not care what kind of task is there and it will execute all 1000 tasks and stops when the queue is finished

while( microTaskQueue.length > 0){
execute (microTaskQueue.getOldestTask())

3rd part :- Render will be blocked until microtask are finished so therefore also render will be blocked when you use lots of promises or it has heavy logic inside it

if( isRenderTime() { render();}

Examples of macro-tasks are setInterval, setImmediate, setTimeout, I/O tasks.

Example of micro-tasks are Promises.

Consider an example :-

setTimeout(function() {
console.log(I am inside setTimeout);
}, 0);
Promise.resolve().then(function() {
console.log(Promise made);
}).then(function() {
console.log(‘Promise kept’’);
console.log(“I will run synchronously”’);

Ideally ‘setTimeout’ should log before promise according to the working of event loop because it would be the first task waiting in the message queue to get executed but due to micro-tasks and macro-tasks logic, the actual output of this example is:-

I will run synchronously

Promise made // Promise is displayed first despite in script it is written as second.

Promise kept

I am inside setTimeout

You may think that setTimeout should be logged first because a macro-task is run first before clearing the micro-task queue. And, when looking back at the script, there is no macro-task enqueued prior to the setTimeout call.

No code runs in JS unless an event has occurred. This event is known as run script and it is queued as a macro-task inside macro-task queue. At the execution of any JS file, the JS engine wraps the contents in a function and associates the function with an event, either start or launch. This is the run script event and is added to the task queue (as a macro-task).

So the flow of the above example is:

  1. Initially:
    LOG (output screen):
    Macro-task : Run Script, SetTimeOutCallBack
    Micro-tasks : promise then, promise then
  2. Now, the first macro-task (run script) is executed:
    LOG (output screen) :   I will run synchronously
    Macro-task: SetTimeOutCallBack
    Micro-tasks: promise then, promise then
  3. Now, the micro-task queue is finished
    LOG (output screen):I will run synchronously
    Promise made
    Promise kept
    Macro-task: SetTimeOutCallBack
  4. Finally, SetTimeOutCallBack is executed
    LOG (output screen) : I will run synchronously
    Promise made
    Promise kept
    I am inside setTimeout

Simplifying Tasks

  1. The job of the event loop is to constantly monitor the message queue (for tasks) and the execution stack and to push the first callback function in-line onto the execution stack, as soon as the stack is empty.
  2. All tasks are not created the same inside message queue. There are macro-tasks and micro-tasks.
  3. Examples of macro-tasks are setInterval, setImmediate, setTimeout, I/O tasks.
  4. Examples of micro-tasks are Promises, process.nextTick.
  5. For each of the ‘event loop’, one macro-task is completed out of the macro-task queue. After the respective macro-task is complete, the event loop visits the micro-task queue and the entire queue is completed before moving on to the next.


Your email address will not be published. Required fields are marked *