Node exits without error and doesn't await promise (Event callback)

Solution 1:

This question is basically: how does node decide whether to exit the event loop or go around again?

Basically node keeps a reference count of scheduled async requests — setTimeouts, network requests, etc.. Each time one is scheduled, that count increases, and each time one is finished, the count decreases. If you arrive at the end of an event loop cycle and that reference count is zero node exits.

Simply creating a promise or event emitter does not increase the reference count — creating these objects isn't actually an async operation. For example, this promise's state will always be pending but the process exits right away:

const p = new Promise( resolve => {
    if(false) resolve()
})

p.then(console.log)

In the same vein this also exits after creating the emitter and registering a listener:

const ev = new EventEmitter()
ev.on("event", (e) => console.log("event:", e))

If you expect Node to wait on an event that is never scheduled, then you may be working under the idea that Node doesn't know whether there are future events possible, but it does because it keeps a count every time one is scheduled.

So consider this small alteration:

const ev = new EventEmitter()
ev.on("event", (e) => console.log("event:", e))

const timer = setTimeout(() => ev.emit("event", "fired!"), 1000)
// ref count is not zero, event loop will go again. 
// after timer fires ref count goes back to zero and node exits

As a side note, you can remove the reference to the timer with: timeout.unref(). This, unlike the previous example, will exit immediately:

const ev = new EventEmitter()
ev.on("event", (e) => console.log("event:", e))

const timer = setTimeout(() => ev.emit("event", "fired!"), 1000)
timer.unref()

There's a good talk about the event loop by Bert Belder here that clears up a lot of misconceptions: https://www.youtube.com/watch?v=PNa9OMajw9w

Solution 2:

I was debugging for several hours why one of our scripts exits (without any errors) after one line of code in the middle of main function. It was a line await connectToDatabase(config). And you know what?

I found that difference between these two functions is CRUCIAL:

first:

async function connectToDatabase(config = {}) {
    if (!config.port) return;
    return new Promise(resolve => {
       resolve();
    })
}

second:

async function connectToDatabase(config = {}) {
    return new Promise(resolve => {
       if (!config.port) return;
       resolve();
    })
}

second function sometimes (when config.port is empty) creates never-resolved promise, it makes event loop empty, and node.js exits thinking that "nothing more to do here"

check it yourself:

// index.js - start it as node index.js
(async function main() {
  console.log('STARTED')
  await connectToDatabase()
  console.log('CONNECTED')
  console.log('DOING SOMETHING ELSE')
})()

'CONNECTED' and 'DOING SOMETHING ELSE' are NOT printed if you use second function and are printed, if you use first