Introduction
Hello Fellow Dev!
Today, we'll discuss the differences between .mjs
(ECMAScript modules) and .cjs
(CommonJS modules) in Node.js. While modern frameworks like React, Next.js, and Vue often handle module support automatically, understanding these differences is crucial when working with Node.js directly, especially regarding the event loop and execution order.
My main goal for this discussion is towards the event loop and in the next sections, we will see some cases.
Basic Information
mjs
(ECMAScript module) supports,
import fs from 'fs'
import https from 'https'
cjs
(CommonJS Modules) supports
const fs = require('fs')
const https = require('https')
Event Loop and Execution Order
The Node.js event loop processes different queues with specific roles and priorities. Two important functions that affect execution order are process.nextTick()
and setImmediate()
and we use these time to time.
process.nextTick vs setImmediate
If you know the difference between process.nextTick
vs setImmediate
that's great if not then, a very basic idea
process.nextTick
ensures that a piece of code runs after the current function but before any asynchronous I/O operations.
setImmediate
schedules a callback function to be executed in the next iteration of the event loop, after any I/O events.
So current code
-> process.nextTick
-> any I/O operations
-> setImmediate
Code Example
Let's examine a code snippet that demonstrates the execution order:
//In case of mjs
import https from "https";
import fs from "fs";
//In case of cjs
const https = require("https");
const fs = require("fs");
setImmediate(() => {
console.log("setImmediate callback");
});
process.nextTick(() => {
console.log("nextTick callback");
});
fs.readFile("./async.cjs", (err, data) => {
console.log("file IO Callback");
});
fs.readdir(process.cwd(), () => console.log("file IO Callback 2"));
https.get("https://www.google.com", (res) => {
console.log("https callback");
});
setImmediate(() => {
console.log("setImmediate callback 2");
});
Promise.resolve().then(() => {
console.log("Promise Callback");
});
process.nextTick(() => {
console.log("Process nextTick console");
process.nextTick(() => {
console.log("Process nextTick console 2");
process.nextTick(() => {
console.log("Process nextTick console 3");
process.nextTick(() => {
console.log("Process nextTick console 4");
});
});
});
});
Promise.resolve().then(() => {
console.log("Promise Callback 2");
});
console.log("Main thread mjs");
Promise.resolve().then(() => {
console.log("Promise Callback 3");
});
Expected vs Actual Execution Order
The code should run and execute in this way
- Main thread
- Promise callbacks
- nextTick callbacks
- setImmediate callbacks
- I/O callbacks and output should be
Main thread mjs
Promise Callback
Promise Callback 2
Promise Callback 3
nextTick callback
Process nextTick console
Process nextTick console 2
Process nextTick console 3
Process nextTick console 4
setImmediate callback
setImmediate callback 2
file IO Callback
file IO Callback 2
https callback
But is it the case with mjs
?
Not Really!
This is the output wrt mjs
and cjs
Similar to process.nextTick
and setImmediate
, we can see the same behaviour with Promises
as well.
What's the reason?
Apparently, the difference in behaviour we're observing between the mjs
(ECMAScript modules) and cjs
(CommonJS modules) files regarding setImmediate
and process.nextTick
is due to how Node.js handles the event loop
and microtasks
in different module systems.
For ESM (.mjs):
- In ESM, Node.js uses a different approach to handle the main module execution.
- The main module code is wrapped in an asynchronous function, which is then executed.
- This causes
setImmediate
callbacks to be scheduled for the next iteration of the event loop, after allmicrotasks
(includingprocess.nextTick
andPromises
) have been processed.
For CommonJS (.cjs):
- In CommonJS, the main module code is executed synchronously.
- This means that
setImmediate
callbacks are scheduled immediately and can run before somemicrotasks
if they're queued early enough.
Framework Behaviour
I have tested this behaviour in the Express and Nextjs app (dev mode) and interestingly, Express behaved like cjs
and Nextjs behaved like mjs
. The first set of logs are from Express and next are from Nextjs
Conclusion
Understanding the differences in execution order between .mjs
and .cjs
files is crucial when working directly with Node.js. I hope, this will help you understand the difference and execution of these functions wrt files, a little bit better. So next time when you play or try these functions in your app, keep these points in mind :)
For another example, please refer to the official Node.js documentation on the differences between ES modules and CommonJS file execution.
Top comments (0)