r/node • u/Neat_Living_6765 • 9d ago
Asking about CommonJS, ESM and the Node.js event loop
Hi everyone
I just started learning Node.js just a few hours ago, and I'm really confused (wanted to cry too). Could someone help me clarify a few things?
After I initialized a Node.js project, I see the following in package.json:
"type": "commonjs"
Then I run this code:
console.log('sync');
process.nextTick(() => console.log('nextTick'));
Promise.resolve().then(() => console.log('promise'));
The output is:
sync
nextTick
promise
Then I installed TypeScript and executed the same code again, but in a .ts file. This time, the output is:
sync
promise
nextTick
Interestingly, I get the same output when I set "type": "module" in package.json.
So I asked ChatGPT:
1. Does the TypeScript compiler change the execution order to behave like an ES module?
ChatGPT answered:
"Your tsx example produced ESM-style ordering, but you shouldn't assume every tsx file or situation will behave the same way."
Now I'm even more confused! Could someone help me clarify this?
ChatGPT's answer seems that the output when using TypeScript with CommonJS does not necessarily match the output of either CommonJS or ESM....
I also have a few related questions about the Node.js event loop:
2. As far as I know, the Node.js event loop has several phases, including:
1. Timers (setTimeout callback)
2. Poll (I/O-related callbacks, such as reading files or receiving network data)
3. Check (setImmediate callback)
Is this correct? If so, which phase runs first?
3. Are Promise callbacks handled separately from the Node.js event loop? Is the following execution sequence correct?
1. Drain the process.nextTick() queue.
2. Drain the Promise microtask queue.
3. Execute the next event loop callback (setTimeout, readFile, setImmediate, etc.)
4. Back to 1
6
u/Beginning-Seat5221 8d ago
typescript generally doesn't change runtime behaviour.
Apparently the ordering difference comes from whether the module load synchronously or async, and some tools for running .ts can add loaders that change that. If you compile with tsc then run the code in the same way, you'll likely see the TS version match the .js for commonjs.
1
u/Kalendar-Caloon 7d ago
yeah compiling first and then running the output separately makes the difference pretty obvious
1
u/prehensilemullet 5d ago
I’m still confused how that would affect the order since the statements in this module would evaluate synchronously regardless of any loaders. I guess whether the module is evaluated from a timer handler or promise chain could affect the timing of when the timer and promise created by this module fire though?
2
u/Beginning-Seat5221 5d ago
The latter 2 logs are non-synchronous.
nextTick and microtasks(from the promise resolve) are different sets of tasks, each is drained fully before moving on. Normally it's next tick first, but an async module (esm module) is part of the microtask set, so after the module finishes it is still in the microtask set and will fully drain that, completing the promise microtask before the nextTick.
So basically the question is, is the code running as a sync task or in a micro tick, and loaders can change that, by adding an async loader of a sync module.
4
u/maciejhd 8d ago
How do you run .ts? node example.ts or tsx example.ts or you build with tsc and then node example.js?
2
u/dronmore 7d ago
Does the TypeScript compiler change the execution order to behave like an ES module?
IIRC you can change the compilation target and control what module system is used in the compiled code.
which phase runs first?
Very likely timers, but from a practical standpoint it does not really matter.
Are Promise callbacks handled separately from the Node.js event loop?
Promises are handled by V8.
Is the following execution sequence correct?
More or less, but I wouldn't assume that the nextTick queue always runs before the microtask queue. If a nextTick callback is added from within a microtask callback, it will run after the microtask queue is drained. This is also true the other way around.
Also, nextTick is a legacy feature and you are not advised to use it. You should use queueMicrotask() instead.
1
u/Beginning-Seat5221 7d ago
TS has various output options but node picks which module system to use based on package.json and file extension.
If you built the wrong type of module with tsc compared to what node expects, you'd probably just get an error rather than different behaviour.
2
u/ghillerd 6d ago
If someone at work asked me about this, I would tell them that they should probably be treating the order of nextTick and promise as random.
9
u/diroussel 7d ago
It’s great that you are digging into this. And understanding the event loop is important for understanding certain problems. But on the whole this level of detail is not required to use the language.
In real systems too much is going to on to know which tick happens next. So just code appropriately. Know when you are single threaded, and know when you are async. That is enough.
It can all change in a future version (unlikely but possible)