Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

I've got a really weird issue whereby awaiting a Promise that has passed its resolve to an event-emitter callback just exits the process without error.

const {EventEmitter} = require('events');  async function main() {   console.log("entry");    let ev = new EventEmitter();    let task =  new Promise(resolve=>{     ev.once("next", function(){resolve()}); console.log("added listener");   });    await task;    console.log("exit"); }  main() .then(()=>console.log("exit")) .catch(console.log);  process.on("uncaughtException", (e)=>console.log(e)); 

I'm expecting the process to halt when I run this because clearly "next" is currently never emitted. but the output I get is:

entry
added listener

and then the nodejs process terminates gracefully.

I thought it was something to do with the Garbage Collector, but ev and task are clearly still in scope on main. So I'm really at a loss as to why the process exits entirely without error.

Obviously I would eventually emit the event, but I've simplified my code to the above to reproduce. I'm on node v8.7.0. Is there something wrong with my code or is this a node bug?

like image 368
Meirion Hughes Avatar asked Oct 24 '17 15:10

Meirion Hughes


People also ask

What happens if you do not await a promise?

Without using await, a promise is returned, which you can use as per need.

What is callback and promise in node JS?

The idea is to create a new Promise object that wraps around the callback function. If the callback function returns an error, we reject the Promise with the error. If the callback function returns non-error output, we resolve the Promise with the output.

Can you await a resolved promise?

await is a new operator used to wait for a promise to resolve or reject. It can only be used inside an async function. Promise. all returns an array with the resolved values once all the passed-in promises have resolved.


2 Answers

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

like image 85
Mark Avatar answered Oct 05 '22 23:10

Mark


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

like image 41
Elena Sharovar Avatar answered Oct 06 '22 00:10

Elena Sharovar