Javascript is a single-threaded language, which mean it can only execute one operation at a time. It uses tasks to manage long operations such as HTTP calls in order to avoid blocking the main thread. But not all tasks have the same priority. Therefore the order in which each operations are performed isn't something trivial.
For those who don't know how tasks are managed in Javascript, I suggest you have a quick look at my article about the event loop. Here, I will focus on the different types and how to trigger them.
Javascript isn't good at providing consistent APIs to schedule tasks. In fact, you'll see that it's quite messy ;)
Synchronous
Synchronous programming is when each statement are executed sequentially. This mean that a statement has to wait until the previous one before executing.
How to call a callback synchronously?
function synchronous(callback) {
callback()
}
Synchronous code is of course the easiest to trigger. We just have to call our callback, and it's done.
Task
Tasks are the oldest building block of asynchronous code in Javascript. They are probably the most known task type 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 a very specific rules to setTimeout. If you start scheduling task within a task, once you reach a certain depth "timeout clamping" will occur. Which mean 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()
}
I won't go into detail, but this code is going to schedule a task immediately whatever the deps of the call.
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 we can see, tasks are always executed once the synchronous code has finished.
Microtask
Microtasks have been introduce lately in Javascript and are well known thanks to Promises. When you resolve a promise a microtask 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 semantic like me, you should be happy with queueMicrotask().
Let's compare the different task and microtask 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
Synchronous code is executed first, then microtasks and then tasks. Microtask has a higher priority than task.
Nanotask
Nanotask 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.
How to trigger a nanotask?
function nanotask(callback) {
process.nextTick(callback)
}
Actually, nanotask is not a real concept in Javascript but it's a easy way to remember the priority order.
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("Nanotask 1"))
// Output
// > Sync 1
// > Nanotask 1
// > Microtask 1
// > Task 1
setImmediate
SetImmediate comes from the golden age of IE but it's no longer part of the browser ecosystem. However it still exist in Node.
function setImmediate(callback) {
setImmediate(callback)
}
Back in the day, setImmediate(callback) was used as a shortcut of setTimeout(callback, 0). What people often ignore is that it triggers a task in a different task queue.
The spec isn't realy clear about the implementation of setImmediate(). To get to the conclusion we need to try the following script.
task(() => console.log("Task 1")) //setTimeout() implementation
task(() => console.log("Task 2")) //MessageChannel() implementation
nanotask(() => console.log("Nanotask 1"))
immediate(() => console.log("Immediate 1"))
synchronous(() => console.log("Sync 1"))
microtask(() => console.log("Microtask 1"))
// Output
// > Sync 1
// > Nanotask 1
// > Microtask 1
// > Task 2
// > Immediate 1
// > Task 1
Has we can see, setImmediate() has a higher priority than setTimeout but a lower priority than MessageChannel. It's easy to argue that setImmediate() is a misleading name for tasks type with almost the least priority amongst all types.