mjs vs cjs with Event Loop

Nikhil Malik - Sep 4 - - Dev Community

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'
Enter fullscreen mode Exit fullscreen mode

cjs (CommonJS Modules) supports

const fs = require('fs')
const https = require('https')
Enter fullscreen mode Exit fullscreen mode

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");
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

But is it the case with mjs?
Not Really!

This is the output wrt mjs and cjs

mjs-vs-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 all microtasks (including process.nextTick and Promises) 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 some microtasks 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

express-vs-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.

. . . .
Terabox Video Player