I was reading about the node's event loop phases, and says that
- timers: this phase executes callbacks scheduled by setTimeout() and setInterval().
- pending callbacks: executes I/O callbacks deferred to the next loop iteration.
- idle, prepare: only used internally.
- poll:retrieve new I/O events; execute I/O related callbacks (almost all with the exception of close callbacks, the ones scheduled by timers, and setImmediate()); node will block here when appropriate.
- check:setImmediate() callbacks are invoked here.
- close callbacks: some close callbacks, e.g. socket.on('close', ...).
So here i have a simple code to test some od the phases above. When you execute the code you get this ouput:
But sockets callbacks as per doc, are last in the phases. Why it is executed first?
let socket = require("net").createServer();
socket.on("data", function (data) {
console.log(data.toString());
});
socket.on("close", function (data) {
console.log("close");
});
socket.listen(8080);
const fs = require("fs");
fs.readFile("readme.txt", () => {
socket.close();
setTimeout(() => {
console.log("timeout");
}, 0);
setImmediate(() => {
console.log("immediate");
});
});
First off, keep in mind that the close
event triggered by you closing the socket locally is not a network operation. It's not triggered by an incoming networking event. It's triggered by the local socket implementation deciding when to fire the close event to notify anyone else monitoring the local socket that it is now closed. It could even fire synchronously (if the net
library that implements it chose to do so). So, anything you've read about how network events are prioritized or sequenced in the event loop does not apply here. This event is not triggered by an incoming network operation and doesn't flow through the event loop as an incoming networking operation would.
I stepped through your code in the debugger and stepped into socket.close()
to try to see what the implementation did. It gets to this line here where is calls this._handle.close()
. That goes into some TCP wrapper layer written in C++ and the debugger won't let you trace into it to see further.
That function then goes on to call emitCloseIfDrained()
which calls defaultTriggerAsyncIdScope()
and is passed process.nextTick()
as the asynchronous mechanism to use.
So, it appears that calling socket.close()
locally causes the socket library to use process.nextTick()
before calling the close
event which will give it a fairly high priority to get processed before other things, but will process on a future tick of the event loop.
But, I hope you can now see how this is super implementation dependent (upon how the event is triggered in whatever library is triggering it) and not documented so should not be relied up in your implementation. It's fine to want to understand as much of this as you can for intellectual curiosity purposes, but you should not design code that relies on this level of implementation detail. A particular feature like when the close
event fires on a socket is not documented and there is no promise that it won't change in the future. If your code requires a particular sequencing of asynchronous events, you need to write your code to manage that sequence to be sure it happens regardless of this level of implementation detail.