node-cleanup vs death vs exit-hook
Graceful Process Termination and Cleanup in Node.js
node-cleanupdeathexit-hookSimilar Packages:

Graceful Process Termination and Cleanup in Node.js

death, exit-hook, and node-cleanup are utility libraries designed to handle graceful shutdowns and cleanup tasks in Node.js applications. They intercept process exit signals (like SIGINT, SIGTERM) and uncaught exceptions to allow developers to run asynchronous cleanup logic (e.g., closing database connections, flushing logs) before the process terminates. While they share a common goal, they differ in API design, signal handling capabilities, and current maintenance status.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
node-cleanup2,191,679163-910 years agoMIT
death0183-310 years ago-
exit-hook030113.2 kB07 months agoMIT

Graceful Process Termination and Cleanup in Node.js

When building Node.js applications, especially servers or long-running scripts, handling process termination correctly is critical. You need to close database connections, flush logs, and release resources before the process dies. The packages death, exit-hook, and node-cleanup all aim to solve this, but they approach the problem with different levels of control and maintenance maturity. Let's break down how they work.

๐Ÿ›‘ Handling Exit Signals and Events

All three packages hook into Node.js process events, but they expose different levels of abstraction.

death listens for SIGINT, SIGTERM, and uncaught exceptions. It wraps the cleanup logic in a simple async function.

// death: Simple async callback
const death = require('death');

death(async ({ signal, err }) => {
  console.log(`Process died with signal: ${signal}`);
  await db.close();
  process.exit(1);
});

exit-hook registers callbacks that run when the process exits. It handles both sync and async functions automatically.

// exit-hook: Registering hooks
const exitHook = require('exit-hook');

exitHook(() => {
  console.log('Exiting');
});

exitHook(async () => {
  await db.close();
});

node-cleanup separates cleanup logic from the exit event itself. You register handlers, and the library manages the exit flow, allowing you to override the exit code.

// node-cleanup: Registration with exit code control
const nodeCleanup = require('node-cleanup');

nodeCleanup((exitCode, signal) => {
  console.log(`Cleanup triggered: ${signal}`);
  db.close();
  // Return true to prevent default exit, false to allow it
  return false; 
});

๐Ÿงน Asynchronous Cleanup Support

Modern Node.js applications often rely on async resources (database pools, network sockets). How each package handles async cleanup varies significantly.

death was one of the early adopters of async cleanup. It awaits the callback before exiting.

// death: Native async support
const death = require('death');

death(async () => {
  await logger.flush();
  await cache.disconnect();
  // Process exits after promise resolves
});

exit-hook also supports async functions out of the box. It waits for all registered hooks to complete before allowing the process to terminate.

// exit-hook: Multiple async hooks
const exitHook = require('exit-hook');

exitHook(async () => {
  await serviceA.stop();
});

exitHook(async () => {
  await serviceB.stop();
});
// Both run before exit

node-cleanup requires you to manage the exit flow manually if you have async operations. The callback is synchronous by default, so you must handle promises explicitly or delay the exit.

// node-cleanup: Manual async handling
const nodeCleanup = require('node-cleanup');

nodeCleanup((exitCode, signal) => {
  cleanupPromise = db.close().then(() => {
    process.exit(exitCode);
  });
  // Prevent default immediate exit
  return true; 
});

โš ๏ธ Maintenance and Stability Status

This is the most critical factor for architectural decisions. Using unmaintained packages for core infrastructure like process lifecycle is risky.

death is deprecated. The repository is archived, and it is no longer receiving updates. It may not handle newer Node.js edge cases correctly.

// death: DEPRECATED
// Do not use in new projects.
// npm install death (Not recommended)

exit-hook is actively maintained. It is part of Sindre Sorhus's ecosystem of high-quality utilities. It receives updates for compatibility with newer Node versions.

// exit-hook: MAINTAINED
// Safe for production use.
// npm install exit-hook

node-cleanup is maintained but has a smaller community footprint compared to exit-hook. It is stable but sees fewer updates.

// node-cleanup: STABLE
// Safe for production use.
// npm install node-cleanup

๐ŸŽฏ Controlling Exit Codes

Sometimes you need to ensure the process exits with a specific code depending on how cleanup went.

death allows you to call process.exit() manually inside the callback, giving you full control.

// death: Manual exit code
const death = require('death');

death(async ({ err }) => {
  if (err) {
    await logError(err);
    process.exit(1);
  }
  process.exit(0);
});

exit-hook does not inherently manage exit codes. It runs hooks and then lets the process exit naturally. You must call process.exit() inside the hook if you want to override.

// exit-hook: Override exit code manually
const exitHook = require('exit-hook');

exitHook(() => {
  if (somethingWrong) {
    process.exit(1);
  }
});

node-cleanup provides the exit code as an argument to the handler, allowing you to inspect it before the process dies.

// node-cleanup: Inspect exit code
const nodeCleanup = require('node-cleanup');

nodeCleanup((exitCode, signal) => {
  if (exitCode !== 0) {
    console.error('Exiting with error code:', exitCode);
  }
  return false; // Proceed with exit
});

๐ŸŒ Real-World Scenarios

Scenario 1: CLI Tool Cleanup

You are building a CLI that creates temporary files. You need to delete them on exit.

  • โœ… Best choice: exit-hook
  • Why? Simple API, actively maintained, handles async file deletion easily.
const exitHook = require('exit-hook');
const fs = require('fs');

exitHook(() => {
  fs.unlinkSync('/tmp/temp-file');
});

Scenario 2: Database Server Shutdown

You run a server that needs to close DB connections gracefully on SIGTERM.

  • โœ… Best choice: node-cleanup
  • Why? Allows inspection of the signal and exit code, useful for logging why the server stopped.
const nodeCleanup = require('node-cleanup');

nodeCleanup((exitCode, signal) => {
  console.log(`Shutting down: ${signal}`);
  db.close();
  return false;
});

Scenario 3: Legacy Script Maintenance

You are maintaining an old script that already uses death.

  • โœ… Best choice: Keep death (temporarily)
  • Why? Refactoring might introduce bugs. Plan to migrate to exit-hook later.
// Legacy code
const death = require('death');
death(() => { /* ... */ });

๐Ÿ“Œ Summary Table

Featuredeathexit-hooknode-cleanup
MaintenanceโŒ Deprecated / Archivedโœ… Activeโœ… Stable
Async Supportโœ… Nativeโœ… Nativeโš ๏ธ Manual Handling
Exit Code Controlโœ… Manual process.exit()โš ๏ธ Manual process.exit()โœ… Inspect in Handler
Signal Handlingโœ… SIGINT, SIGTERMโœ… Process Exit Eventsโœ… SIGINT, SIGTERM, Uncaught
API Complexity๐ŸŸข Low๐ŸŸข Low๐ŸŸก Medium

๐Ÿ’ก Final Recommendation

For new projects, always choose exit-hook. It is actively maintained, has a clean API, and handles async cleanup reliably without forcing you to manage exit flows manually unless you need to.

Use node-cleanup if you specifically need to inspect the exit code or signal within the cleanup handler before the process terminates, as it exposes these arguments directly.

Avoid death in any new architecture. It is deprecated and unmaintained, posing a risk for long-term stability. If you encounter it in legacy code, plan a migration to exit-hook during your next refactor cycle.

Final Thought: Process cleanup is infrastructure code. It needs to be reliable. Prioritize maintenance status and community support over minor API differences when choosing between these utilities.

How to Choose: node-cleanup vs death vs exit-hook

  • node-cleanup:

    Choose node-cleanup if you require explicit control over exit codes and need to handle uncaught exceptions alongside standard exit signals. It provides a structured registration method for cleanup handlers and allows modifying the exit code before termination. It is suitable for server applications where distinguishing between clean and error exits matters.

  • death:

    Choose death only for legacy projects already depending on it, as the package is deprecated and no longer maintained. It offers a simple async callback interface but lacks modern signal handling robustness. For new projects, avoid this package due to potential security or stability risks associated with unmaintained code.

  • exit-hook:

    Choose exit-hook if you need a lightweight, well-maintained solution for running synchronous or asynchronous tasks before process exit. It is actively maintained by Sindre Sorhus and supports standard exit signals. It is ideal for CLI tools or scripts where you need to ensure cleanup runs without complex configuration.

README for node-cleanup

node-cleanup

installs custom cleanup handlers that run on exiting node

Installation

npm install node-cleanup --save

Overview

nodeCleanup() installs functions that perform cleanup activities just before the node process exits. Let's call these functions "cleanup handlers." The cleanup handlers run under the following conditions:

  • When the process exits normally (exit code 0).
  • When the process exits due to an error, such as an uncaught exception (exit code 1).
  • When the process receives one of the following POSIX signals: SIGINT (e.g. Ctrl-C), SIGHUP, SIGQUIT, or SIGTERM.

This solution has the following features:

  • Allows cleanup handlers to behave as a function of exit code and signal.
  • Allows multiple independent subsystems to install cleanup handlers.
  • Allows for asynchronous cleanup on receiving a signal by postponing process termination.
  • Allows for deferring to child processes the decision about whether a signal terminates the present process. For example, Emacs intercepts Ctrl-C, which should prevent its parent process from terminating.
  • Allows for writing custom messages to stderr on SIGINT (e.g. Ctrl-C) and uncaught exceptions, regardless of the number of cleanup handlers installed.
  • Allows for uninstalling all cleanup handlers, such as to change termination behavior after having intercepted and cleaned up for a signal.

The module also has an extensive test suite to help ensure reliability.

Usage

Here is the typical way to use nodeCleanup():

var nodeCleanup = require('node-cleanup');

nodeCleanup(function (exitCode, signal) {
    // release resources here before node exits
});

If you only want to install your own messages for Ctrl-C and uncaught exception (either or both), you can do this:

nodeCleanup({
    ctrl_C: "{^C}",
    uncaughtException: "Uh oh. Look what happened:"
});

To get the default stderr messages, without installing a cleanup handler:

nodeCleanup();

You may also combine these to install a cleanup handler and stderr messages:

nodeCleanup(function (exitCode, signal) {
    // release resources here before node exits
}, {
    ctrl_C: "{^C}",
    uncaughtException: "Uh oh. Look what happened:"
});

You may perform asynchronous cleanup upon receiving a signal, as follows:

nodeCleanup(function (exitCode, signal) {
    if (signal) {
        unsavedData.save(function done() {
            // calling process.exit() won't inform parent process of signal
            process.kill(process.pid, signal);
        });
        nodeCleanup.uninstall(); // don't call cleanup handler again
        return false;
    }
});

When you hit Ctrl-C, you send a SIGINT signal to each process in the current process group. A process group is set of processes that are all supposed to end together as a group instead of persisting independently. However, some programs, such as Emacs, intercept and repurpose SIGINT so that it does not end the process. In such cases, SIGINT should not end any processes of the group. Here is how you can delegate the decision to terminate to a child process:

var nodeCleanup = require('node-cleanup');
var fork = require('child_process').fork;

var child = fork('path-to-child-script.js');
child.on('exit', function (exitCode, signal) {
    child = null; // enable the cleanup handler
    if (signal === 'SIGINT')
        process.kill(process.pid, 'SIGINT');
});

nodeCleanup(function (exitCode, signal) {
    if (child !== null && signal === 'SIGINT')
        return false; // don't exit yet
    // release resources here before node exits
});

Reference

nodeCleanup()

nodeCleanup() has the following available (FlowType) signatures:

function nodeCleanup(cleanupHandler: Function): void
function nodeCleanup(cleanupHandler: Function, stderrMessages: object): void
function nodeCleanup(stderrMessages: object): void
function nodeCleanup(): void

The 1st form installs a cleanup handler. The 2nd form also assigns messages to write to stderr on SIGINT or an uncaught exception. The 3rd and 4th forms only assign messages to write to stderr, without installing a cleanup handler. The 4th form assigns default stderr messages.

cleanupHandler is a cleanup handler callback and is described in its own section below. When no cleanup handlers are installed, termination events all result in the process terminating, including signal events.

stderrMessages is an object mapping any of the keys ctrl_C and uncaughtException to message strings that output to stderr. Set a message to the empty string '' inhibit a previously-assigned message.

nodeCleanup() may be called multiple times to install multiple cleanup handlers or override previous messages. Each handler gets called on each signal or termination condition. The most recently assigned messages apply.

nodeCleanup.uninstall()

nodeCleanup.uninstall() uninstalls all installed cleanup handlers and voids the stderr message assignments. It may be called multiple times without harm.

This function is primarily useful when a signal occurs and the cleanup handler performs cleanup but disables immediate process termination. In this case, when it is finally time to terminate the process, the cleanup handlers shouldn't run again, so the process uninstalls the handlers before terminating itself.

Cleanup Handlers

Each cleanup handler has the following (FlowType) signature:

function cleanupHandler(exitCode: number|null, signal: string|null): boolean?

If the process is terminating for a reason other than a POSIX signal, exitCode is the exit code, and signal is null. Otherwise, if the process received a signal, signal is the signal's string name, and exitCode is null. These are the arguments passed to a child process exit event handler, mirrored here in node-cleanup for consistency.

Node.js defines these standard exit codes, but it does not appear to use code values >128 for signals. According to the node.js docs, these are the possible signals, but the cleanup handlers only run on SIGINT (e.g. Ctrl-C), SIGHUP, SIGQUIT, or SIGTERM. (It is not possible to intercept SIGKILL.)

The return value of a cleanup handler is only significant for signals. If any cleanup handler returns a boolean false, the process does not exit. If they all return true (or for backwards compatibility, no return value), the process exits, reporting the signal to the parent process as the reason for the exit. The process always exits after calling the cleanup handlers for non-signals.

When a cleanup handler returns false to prevent the process from exiting, the cleanup handler normally takes steps to ensure proper termination later. For example, the process may wait for asynchronous cleanup to complete, or it may wait for a child process to signal termination. Normally in these cases the process would use nodeCleanup.uninstall() to uninstall the cleanup handlers prior to the second termination to prevent them from running again.

A cleanup handler should never call process.exit(). If a handler prevents a signal from terminating the process but later wishes to terminate the process for reason of this signal, the process should call process.kill(process.pid, signal). In particular, the process should not call process.exit(128 + signalNumber), because while this does communicate the exit code to the parent process, it does not communicate the exit signal by the means that the node.js child_process expects.

Testing

This module includes an extensive test suite. You can run it from the module directory with either the tap or subtap test runner, as follows:

npm install -g tap
npm install
tap tests/*.js

or

npm install -g subtap
npm install
subtap

(As of this writing, the test suite has only been run on a Mac. Behavior may vary from OS to OS, so I'm looking for feedback from other operating systems.)

Incompatibilities with v1.0.x

node-cleanup v2+ is not fully compatible with v1.x. You may need to change your usage to upgrade. These are the potential incompatibilities:

  • The cleanup handlers now also run on SIGHUP, SIGQUIT, and SIGTERM, which were not getting cleanup processing before.
  • stderr messages are handled quite differently. Previously, there were defaults that you had to override, and only your first message assignments applied. Now, the defaults only install with the parameterless call nodeCleanup(). Otherwise there are no messages unless you provide them. Moreover, the most recent message assignments are the ones that get used.

Acknowledgements

This module began by borrowing and modifying code from CanyonCasa's answer to a stackoverflow question. I had found the code necessary for all my node projects. @Banjocat piped in with a comment about how the solution didn't properly handle SIGINT. (See this detailed explanation of the SIGINT problem). I have completely rewritten the module to properly deal with SIGINT and other signals (I hope!). The rewrite also provides some additional flexibility that @zixia and I found ourselves needing for our respective projects.

License

This license applies to v2 and later. v1 derived from this stackoverflow answer.

MIT License

Copyright (c) 2016 Joseph T. Lapp

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.