The real answer finally came from Bert Belder, who worked on libuv.
Every time Node queues an action that will resolve at some point in the future, think async functions or timers, it has a ref – a mark to remind itself it’s waiting on some future work. The event loop checks if there are any refs. If the ref counter is 0 , Node exits the process. If there are refs, the event loop goes back to the start and does its thing. You can think of it as a super simple while(refCounter)
loop.
So, what happens with a server? This exact process, except the listen function doesn’t decrement the ref counter. The event loop sees there’s always at least one ref left, so it never ends the process instead goes back to the start and loops until something new happens.
You can see this by using server.unref()
on the server object with the net or http modules. The unref method, available on other objects as well, tells Node to ignore the ref for that specific function when counting at the end of the event loop. If you run the code below you’ll see Node closes the process, even though it starts listening to the port.
const http = require("http");
http
.createServer((req, res) => {
res.writeHead(200);
res.end("Bye bye");
})
.listen(3333)
.unref();