Javascript: Macro, Micro and Nano tasks

Introduction

Javascript is a single-threaded language, so it can only execute one operation at a time. It uses a javascript scheduler to manage long operations such as HTTP calls in order to avoid blocking the main thread.

To understand this you need to know how the event loop handles tasks and microtasks. This can be a lot to get your head around the first time you encounter it.

Task or Macro task

Tasks are the oldest building block of asynchronous code in Javascript. To have a better understanding, here is some examples when tasks get scheduled:
A new JavaScript program or subprogram is executed (such as from a console, or by running the code in a script element.
An event is fired by the user, adding the event's callback function to the task queue.
A timeout or interval created with setTimeout or setInterval is reached, causing the corresponding callback to be added to the task queue.

How to schedule a task easily?

function task(callback) {
  setTimeout(() => callback(), 0)
}

This technique works well but the spec mentions very specific rules to setTimeout. If you start scheduling a task within a task, once you reach a certain depth "timeout clamping" will occur. Which means the task won't schedule immediately but it will start scaling it after 4 ms.

A more reliable way is to use MessageChannel.

function task(callback) { 
  const mc = new MessageChannel()
  mc.port1.postMessage(null)
  mc.port2.addEventListener("message", () => {
      callback()
  }, {once: true })
  mc.port2.start()
}

Now, let's execute a synchronous code and a task in the same script.

task(() => console.log("Task 1"))
synchronous(() => console.log("Sync 1"))

// Output
// > Sync 1
// > Task 1

As you can see in the above code, tasks are executed after synchronous code has finished.

Micro task

Micro tasks were introduced later in Javascript and are well known thanks to Promises. When you resolve a promise a micro-task is scheduled.

function microtask(callback) {
  Promise.resolve().then(() => callback())
}

More recently, a much cleaner way came out.

function microtask(callback) {
  queueMicrotask(callback)
}

If you're a fan of semantics, you should be happy with queueMicrotask().

Let's compare the different tasks and micro tasks with this script.

task(() => console.log("Task 1"))
microtask(() => console.log("Microtask 1"))
synchronous(() => console.log("Sync 1"))

// Output
// > Sync 1
// > Microtask 1
// > Task 1

In above code, synchronous code is executed first and then micro tasks and then tasks.

Nano task

Nano task comes from process.nextTick() which is only available in Node. If you've already understood the difference between tasks, and microtasks, you should already guess that the name comes from the fact that it has a higher priority than microtask.

function nanotask(callback) {
  process.nextTick(callback)
}

Surprisingly, nano tasks are not a real concept in Javascript but it's an easy way to remember the priority order.

How Javascript Scheduler prioritizes tasks, micro tasks and nano tasks

Lets start with an example

Script-start => script-end => process.nextTick(nano) => promise(micro) => setTimeout(macro/task)

The script above shows the priority order in Node.

task(() => console.log("Task 1"))
microtask(() => console.log("Microtask 1"))
synchronous(() => console.log("Sync 1"))
Nanotask(() => console.log("Nano Task 1"))
// Output
// > Sync 1
// > Nano Task 1
// > Microtask 1
// > Task 1

This shows that the nano tasks will be executed first, followed by micro task and macro task.

If you are a tech geek and have applied this concept, tell us about your experience in the comments below about dealing with event loop in JavaScript. And those who are new to this concept, feel free to ask more about anything which boggles your mind regarding this post!