# Quick tip: Task execution order changes based on module type

2 min read

I was recently reading a bit about the internals of the Nodejs event loop (check out the official docs). I won’t get into the details of how the event loop works, but the official docs linked above and a nice blog post (check it out here) provide a high-level explanation of the main concepts and the way the event loop works.

I wanted to share an example of a subtle difference in the execution order of different things (macrotasks, microtasks, etc) that I found interesting. Here is the code:

const { createHash } = require('node:crypto')
function main() {
console.log('In main function (1)')
// A macro task
setTimeout(() => {
console.log('In timeout')
}, 0)
// A promise (micro task)
const p = new Promise((resolve, _) => {
const secret = createHash('sha256').update('supersecret').digest()
resolve(secret)
})
p.then((_) => console.log('Promise resolved'))
// Next tick callbacks
process.nextTick(() => {
console.log('Next tick')
})
process.nextTick(() => {
console.log('Another Next tick callback')
})
console.log('In main function (2)')
}
main()

The output of this code when running node <filename.js> is:

Terminal window
In main function (1)
In main function (2)
Next tick
Another Next tick callback
Promise resolved
In timeout

This is what I expected, too: the main function is executed, then next tick callbacks (find out more about it here), the promise callbacks and finally the timeout callback. In general, the order of execution is: the oldest macrotask in the macrotask queue, then next tick callbacks, then all the microtasks, then the oldest macrotask and so on.

So far, so good. Let’s now make a small change and switch to ES modules instead of commonjs. We’ll only need to change the first line of the script to this:

import { createHash } from 'node:crypto'

When we run the script again, the output changes to this:

Terminal window
In main function (1)
In main function (2)
Promise resolved
Next tick
Another Next tick callback
In timeout

Promise callbacks get executed first and next tick callbacks afterwards! Why is that? Searching for an answer, the official docs explain it very well (check the documentation for more details):

So in CJS modules process.nextTick() callbacks are always run before queueMicrotask() ones. However since ESM modules are processed already as part of the microtask queue, there queueMicrotask() callbacks are always executed before process.nextTick() ones since Node.js is already in the process of draining the microtask queue.
My avatar

Thanks for reading my blog post! Feel free to check out my other posts or contact me via the social links in the footer.


More Posts

# How to benchmark your ruby code

7 min read

I’ve recently started a new personal project (a web application), and decided to build it using Ruby on Rails. I’m a big fan of using boring technology to solve problems and, while I’ve been working…

Read