Search code examples
node.jspromiseasync-awaites6-promise

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


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"!


Solution

  • 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).