4

I discovered an odd behaviour in node's promisify() function and I cannot work out why it's doing what it's doing.

Consider the following script:

#!/usr/bin/env node

/**
 * Module dependencies.
 */

var http = require('http')

var promisify = require('util').promisify

;(async () => {
  try {

    // UNCOMMENT THIS, AND NODE WILL QUIT
    // var f = function () { return 'Straight value' }
    // var fP = promisify(f)
    // await fP()

    /**
     * Create HTTP server.
     */
    var server = http.createServer()

    /**
     * Listen on provided port, on all network interfaces.
     */

    server.listen(3000)
    server.on('error', (e) => { console.log('Error:', e); process.exit() })
    server.on('listening', () => { console.log('Listening') })
  } catch (e) {
    console.log('ERROR:', e)
  }
})()

console.log('OUT OF THE ASYNC FUNCTION')

It's a straightforward self-invoking function that starts a node server. And that's fine.

NOW... if you uncomment the lines under "UNCOMMENT THIS", node will quit without running the server.

I KNOW that I am using promisify() on a function that does not call the callback, but returns a value instead. So, I KNOW that that is in itself a problem.

However... why is node just quitting...?

This was really difficult to debug -- especially when you have something more complex that a tiny script.

If you change the function definition to something that actually calls a callback:

var f = function (cb) { setTimeout( () => { return cb( null, 'Straight value') }, 2000) }

Everything works as expected...

UPDATE

Huge simplification:

function f () {
  return new Promise(resolve => {
    console.log('AH')
  })
}

f().then(() => {
  console.log('Will this happen...?')
})

Will only print "AH"!

Merc
  • 16,277
  • 18
  • 79
  • 122
  • Hmmm interesting. I assume that after some time of an empty event loop (Waiting for a promise that never resolves) Node just moves on. And since you have nothing else listening for input, it acts like a script and just closes? – coagmano Mar 02 '18 at 04:53
  • That was my theory, but it doesn't explain why using a *proper* callback-calling function, it works! – Merc Mar 02 '18 at 04:55
  • [the callback style is required IMHO](https://hackernoon.com/node8s-util-promisify-is-so-freakin-awesome-1d90c184bf44). – lloyd Mar 02 '18 at 04:57
  • @lloyd, read the part starting with "I KNOW". - he knows – coagmano Mar 02 '18 at 04:59
  • 1
    The point of this question is to identify a possible problem with node, and -- in the future -- make it easier for developers to identify such a problem. Realistically, this shouldn't happen. I would like to know what the wider SO community thinks about this before I open a proper bug in node (and potentially embarrass myself!) – Merc Mar 02 '18 at 05:03
  • I would be amazed if Node was smart enough to determine that no path to completion exists and exit, but if so a notice would be nice – coagmano Mar 02 '18 at 05:13
  • It doesn't exit with an error code – coagmano Mar 02 '18 at 05:14
  • Maybe inspect the JIT that V8 generates? – coagmano Mar 02 '18 at 05:18

1 Answers1

5

Call promisify() on a non-callback function: “interesting” results in node. Why?

Because you allow node.js to go to the event loop with nothing to do. Since there are no live asynchronous operations in play and no more code to run, node.js realizes that there is nothing else to do and no way for anything else to run so it exits.

When you hit the await and node.js goes back to the event loop, there is nothing keeping node.js running so it exits. There are no timers or open sockets or any of those types of things that keep node.js running so the node.js auto-exit-detection logic says that there's nothing else to do so it exits.

Because node.js is an event driven system, if your code returns back to the event loop and there are no asynchronous operations of any kind in flight (open sockets, listening servers, timers, file I/O operations, other hardware listeners, etc...), then there is nothing running that could ever insert any events in the event queue and the queue is currently empty. As such, node.js realizes that there can never be any way to run any more code in this app so it exits. This is an automatic behavior built into node.js.

A real async operation inside of fp() would have some sort of socket or timer or something open that keeps the process running. But because yours is fake, there's nothing there and nothing to keep node.js running.

If you put a setTimeout() for 1 second inside of f(), you will see that the process exit happens 1 second later. So, the process exit has nothing to do with the promise. It has to do with the fact that you've gone back to the event loop, but you haven't started anything yet that would keep node.js running.

Or, if you put a setInterval() at the top of your async function, you will similarly find that the process does not exit.


So, this would similarly happen if you did this:

var f = function () { return 'Straight value' }
var fP = promisify(f);
fP().then(() => {
    // start your server here
});

Or this:

function f() {
    return new Promise(resolve => {
       // do nothing here
    });
}

f().then(() => {
    // start your server here
});

The issue isn't with the promisify() operation. It's because you are waiting on a non-existent async operation and thus node.js has nothing to do and it notices there's nothing to do so it auto-exits. Having an open promise with a .then() handler is not something that keeps node.js running. Rather there needs to be some active asynchronous operation (timer, network socket, listening server, file I/O operation underway, etc...) to keep node.js running.

In this particular case, node.js is essentially correct. Your promise will never resolve, nothing else is queued to ever run and thus your server will never get started and no other code in your app will ever run, thus it is not actually useful to keep running. There is nothing to do and no way for your code to actually do anything else.

If you change the function definition to something that actually calls a callback:

That's because you used a timer so node.js has something to actually do while waiting for the promise to resolve. A running timer that has not had .unref() called on it will prevent auto-exit.

Worth reading: How does a node.js process know when to stop?


FYI, you can "turn off" or "bypass" the node.js auto-exit logic by just adding this anywhere in your startup code:

// timer that fires once per day
let foreverInterval = setInterval(() => {
    // do nothing
}, 1000 * 60 * 60 * 24);

That always gives node.js something to do so it will never auto-exit. Then when you do want your process to exit, you could either call clearInterval(foreverInterval) or just force things with process.exit(0).

jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • @Merc - I'm not sure what you're asking with that code? You never resolve your promise so the `.then()` handler will never get called. Likewise, there are no asynchronous operations actually scheduled with that code so node.js will exit. The mere creation of a promise does not keep node.js from exiting. You need a LIVE asynchronous operation to keep node.js from exiting. – jfriend00 Mar 02 '18 at 05:54
  • The code was a mistaken paste -- sorry. Please see the update I gave on the question. You are absolutely right -- I would have never expected that!!! So... what's actually happening with the UPDATE code...?!? `f()` obviously runs, and returns a promise. The code in the Promise's callback is executed (so you see "AH"). And then... surely, surely it should execute what's in the "then"?!? – Merc Mar 02 '18 at 05:57
  • I mean, `that f().then` is _code_ that still hasn't been executed! Surely node should at least _execute_ the code in the rest of the script! I realise that node needs to work out when to "stop"; but surely that should apply once _ALL_ of the code has been run! Would you be able to describe what _actually_ happens? – Merc Mar 02 '18 at 05:58
  • @Merc - That's not how it works. The internals of node.js keep track of actual async operations that are pending. It keeps a reference count. `.then()` handlers are treated just like an event listener on an `EventEmitter`. They don't count in this at all. All that counts are actual asynchronous operation still in flight. And, there's a good reason for this. If node.js gets to the event loop and there are no actual async operations running, then node.js can never run any more code. Never. So, it might as well exit. – jfriend00 Mar 02 '18 at 05:59
  • @Merc - This happens because once node.js gets to the event loop, the ONLY way it can ever do anything else is when an event gets put in the event loop. But, the only way that can happen is if there's an async operation of some type in flight that could cause an event to get put in the event loop. So, if there are none of those in flight, then nothing will ever get put in the event queue so there's nothing for the event loop to ever do. May as well exit. – jfriend00 Mar 02 '18 at 06:02