Node, exit and asynchronous cleanup

Node can exit for three main reasons:

  • implicitly: when there is no more code to be executed AND the event loop runs dry (i.e. there is no outstanding timer and I/O listener).
  • explicitly: when the code execute a process.exit().
  • accidentally: an uncaught exception bubbles up to the event loop, and there is no uncaught exception listener on the process.

If you are explicitly closing the app, you can trigger whatever cleanup code you need, before calling process.exit().

However there are a lot of case where you can't predict at all when your program will exit: e.g. if there is simply no more job scheduled in the event loop, your program has done the job and will exit.

If some cleanup code has to be performed, we can listen for the exit event on the process object. However, the program will exit as soon as all listeners have returned: only synchronous tasks can be perfomed here!

But the very nature of Node.js is asynchronous: lot of stuff we would want to do cannot be done synchronously.

For example: we want to log the context and status when exiting, but one of our transport is async (e.g. it logs to a database or over the network). If we send data over the wire from that kind of listener, there are good chance it will never reach its destination.

Introducing async-kit

I will sound like I'm doing (again) my self-promotion here, but there is a great package (sic!) that do the job: async-kit. This is the first package I ever wrote. This is a toolbox that deals with whatever async stuff you have to do, and it has been tested for nearly two years now... and it still receives few improvements once in a while!

The last feature addition it received is the async.exit() method. This is a replacement for process.exit(), and its purpose is to exit asynchronously.


async.exit( code , timeout )

  • code number the exit code
  • timeout number the maximum time allowed for each underlying listener before aborting, default to 1000 (ms).

When you call async.exit(), it emits the asyncExit event on the process object.

There are two kinds of listeners:

  • function( [code] , [timeout] ) listeners, that does not have a callback, are interested in the event but they don't need to perform critical tasks or can handle them synchronously. E.g.: a server that will not accept connection or data anymore after receiving this event.

  • function( code , timeout , completionCallback ) listeners, that DO have a completion callback, have some critical asynchronous tasks to perform before exiting. E.g.: a server that needs to gracefully exit will not accept connection or data anymore, but it still has to wait for client request in progress to be done.

Note that the code and timeout arguments passed to listeners are actual values used by async.exit().

So async.exit() will simply wait for all listeners having a completionCallback to trigger it (or being timed out) before exiting, using process.exit() internally.

process.on( 'asyncExit' , function( code , timeout , callback ) {

    console.log( 'asyncExit event received - starting a short task' ) ;

    setTimeout( function() {
        console.log( 'Short task finished' ) ;
        callback() ;
    } , 100 ) ;
} ) ;

process.on( 'asyncExit' , function( code , timeout ) {

    console.log( 'asyncExit event received - non-critical task' ) ;

    setTimeout( function() {
        console.log( 'Non-critical task finished' ) ;
    } , 200 ) ;
} ) ;

async.exit( 5 , 500 ) ;

After 100ms, it will produce:

asyncExit event received - starting a short task
asyncExit event received - non-critical task
Short task finished

Note how the setTimeout()'s function is not executed in the second event handler: this handler does not accept a callback, hence the process will exit as soon as the first handler is done: after 100ms.

Nice, isn't it?


Now you will never use process.exit() anymore!

Happy coding!