47

I've been trying to perform an asynchronous operation before my process is terminated.

Saying 'terminated' I mean every possibility of termination:

  • ctrl+c
  • Uncaught exception
  • Crashes
  • End of code
  • Anything..

To my knowledge the exit event does that but for synchronous operations.

Reading the Nodejs docs i found the beforeExit event is for the async operations BUT :

The 'beforeExit' event is not emitted for conditions causing explicit termination, such as calling process.exit() or uncaught exceptions.

The 'beforeExit' should not be used as an alternative to the 'exit' event unless the intention is to schedule additional work.

Any suggestions?

Community
  • 1
  • 1
Kesem David
  • 2,135
  • 3
  • 27
  • 46
  • 3
    How about putting all the code in separate file (e.g. afterExit.js)and when an exit event occurs then run `require('child_process').exec('node afterExit.js');`, it's just a thought though – Molda Nov 13 '16 at 13:39
  • @Molda hey again, I left that issue for a while and now back on working, figured using and external script to be executed like you suggested would be an easy solution, but i came accross a misunderstanding of how to pass any variables to that script, since that 'afterExit' code is depends on the parent's state, any help would be much appreciated! – Kesem David Dec 13 '16 at 10:59
  • You can pass some variables as params e.g. `exec('node afterExit.js some_param some_other_param');` and access then with `process.args` or alternatively create a json file with required data and pass path to that file to your script and load that file from afterExit.js – Molda Dec 13 '16 at 11:05
  • @Molda I'm back after few days of dealing with that `child_process` exec, spawn. fork methods, would really apreaciate if you could take a look at this question i have just posted http://stackoverflow.com/questions/41209901/how-to-fork-a-process-of-another-module – Kesem David Dec 18 '16 at 15:36

5 Answers5

28

You can trap the signals and perform your async task before exiting. Something like this will call terminator() function before exiting (even javascript error in the code):

process.on('exit', function () {
    // Do some cleanup such as close db
    if (db) {
        db.close();
    }
});

// catching signals and do something before exit
['SIGHUP', 'SIGINT', 'SIGQUIT', 'SIGILL', 'SIGTRAP', 'SIGABRT',
    'SIGBUS', 'SIGFPE', 'SIGUSR1', 'SIGSEGV', 'SIGUSR2', 'SIGTERM'
].forEach(function (sig) {
    process.on(sig, function () {
        terminator(sig);
        console.log('signal: ' + sig);
    });
});

function terminator(sig) {
    if (typeof sig === "string") {
        // call your async task here and then call process.exit() after async task is done
        myAsyncTaskBeforeExit(function() {
            console.log('Received %s - terminating server app ...', sig);
            process.exit(1);
        });
    }
    console.log('Node server stopped.');
}

Add detail requested in comment:

  • Signals explained from node's documentation, this link refers to standard POSIX signal names
  • The signals should be string. However, I've seen others have done the check so there might be some other unexpected signals that I don't know about. Just want to make sure before calling process.exit(). I figure it doesn't take much time to do the check anyway.
  • for db.close(), I guess it depends on the driver you are using. Whether it's sync of async. Even if it's async, and you don't need to do anything after db closed, then it should be fine because async db.close() just emits close event and the event loop would continue to process it whether your server exited or not.
Ben
  • 5,024
  • 2
  • 18
  • 23
  • Hey Ben, thanks for your answer. Could you provide some more explanation about this code? the signals im familiar with are `SIGINT` and `exit`, what are the others? also, why are u checking if the `sig` is a `string`? and assuming the `db.close` is async it wont be finished there right? – Kesem David Nov 14 '16 at 07:28
  • To be honest, I don't know since I haven't use Windows for over a decade but I think it should work because this is standard node.js code. Node.js on Windows should know how to interpret it. – Ben Nov 29 '18 at 23:15
  • 2
    @KesemDavid is actually right here. In the (very likely) event that `db.close()` is async, calling if from an `'exit'` event handler [does not guarantee its execution](https://nodejs.org/dist/latest/docs/api/process.html#process_event_exit). Checkout some of the answers below for more rigorous implementations of this. – Nicolás Fantone Sep 23 '20 at 13:21
  • Is `SIGILL` meant to be `SIGKILL`? – Nick Bull Mar 11 '21 at 18:27
  • As @NicolásFantone mentioned, the event handler is sync. The reference he linked says "Listener functions must only perform synchronous operations." Your terminator fn is called, but it is not guaranteed to execute the async work to completion. – darcyparker Sep 10 '21 at 15:01
  • too many unnecessary event handlers, from reading that documentation it seems you need to handle SIGINT, SIGTERM, SIGHUP – PirateApp Dec 23 '22 at 14:03
25

Using beforeExit hook

The 'beforeExit' event is emitted when Node.js empties its event loop and has no additional work to schedule. Normally, the Node.js process will exit when there is no work scheduled, but a listener registered on the 'beforeExit' event can make asynchronous calls, and thereby cause the Node.js process to continue.

process.on('beforeExit', async () => {
    await something()
    process.exit(0) // if you don't close yourself this will run forever
});
Ben Swinburne
  • 25,669
  • 10
  • 69
  • 108
Sceat
  • 1,427
  • 14
  • 12
  • 12
    They say in the docs that 'beforeExit' is not emitted when process.exit() is explicitly called somewhere in the code, or when unaught exceptions happen, which makes its usage quite limited. Source: https://nodejs.org/api/process.html#process_event_beforeexit – Alexey Grinko Dec 09 '20 at 18:20
8

Here's my take on this. A bit long to post as a code snippet in here, so sharing a Github gist.

https://gist.github.com/nfantone/1eaa803772025df69d07f4dbf5df7e58

It's pretty straightforward. You use it like so:

'use strict';
const beforeShutdown = require('./before-shutdown');

// Register shutdown callbacks: they will be executed in the order they were provided
beforeShutdown(() => db.close());
beforeShutdown(() => server.close());
beforeShutdown(/* Do any async cleanup */);

The above will listen for a certain set of system signals (SIGINT, a.k.a Ctrl + C, and SIGTERM by default) and call each handler in order before shutting down the whole process.

It also,

  • Supports async callbacks (or returning a Promise).
  • Warns about failing shutdown handlers, but prevents the error/rejection from bubbling up.
  • Forces shutdown if handlers do not return after some time (15 seconds, by default).
  • Callbacks can be registered from any module in your code base.
Nicolás Fantone
  • 2,055
  • 1
  • 18
  • 24
  • 3
    Can you clarify how it supports async callbacks? Your gist relies on `process.once()` . The handler for `process.once('SIGTERM', handler)` is a sync function. I am sure the async callback is called, but it is not guaranteed to run until its returned promise is fullfilled. Do you have tests showing it will? – darcyparker Sep 10 '21 at 15:15
6

Combining answers + handling for uncaught exceptions and promise rejections

async function exitHandler(evtOrExitCodeOrError: number | string | Error) {
  try {
    // await async code here
    // Optionally: Handle evtOrExitCodeOrError here
  } catch (e) {
    console.error('EXIT HANDLER ERROR', e);
  }

  process.exit(isNaN(+evtOrExitCodeOrError) ? 1 : +evtOrExitCodeOrError);
}

[
  'beforeExit', 'uncaughtException', 'unhandledRejection', 
  'SIGHUP', 'SIGINT', 'SIGQUIT', 'SIGILL', 'SIGTRAP', 
  'SIGABRT','SIGBUS', 'SIGFPE', 'SIGUSR1', 'SIGSEGV', 
  'SIGUSR2', 'SIGTERM', 
].forEach(evt => process.on(evt, exitHandler));
Mike Ohlsen
  • 1,900
  • 12
  • 21
Ron S.
  • 563
  • 5
  • 13
4

You need to trap beforeExit, uncaughtException, and each of the listenable signals that can terminate the process: SIGHUP, SIGINT, SIGTERM, SIGUSR2 (Nodemon), and SIGBREAK (Windows). (You likely don't want to clean up on SIGQUIT, since that signal is used for core dumps.) After your async operations have completed, you then need to explicitly terminate the process using the appropriate mechanism, such as process.exit or process.kill. For SIGINT in particular, it's important to propagate the signal to the parent process (i.e. using process.kill as opposed to process.exit). Also note that you need to stop trapping the signals before calling process.kill. Because this is all relatively tricky, I published a library, async-cleanup, to make adding async exit hooks as easy as function call:

import { addCleanupListener } from "async-cleanup";
import { unlink, writeFile } from "fs/promises";

await writeFile(".lockfile", String(process.pid), { flag: "wx" });
addCleanupListener(async () => {
  await unlink(".lockfile");
});
Trevor Robinson
  • 15,694
  • 5
  • 73
  • 72