death vs exit-hook vs node-cleanup
Graceful Process Termination and Cleanup in Node.js
deathexit-hooknode-cleanupSimilar 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
death0183-39 years ago-
exit-hook030213.2 kB0a month agoMIT
node-cleanup0166-99 years 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: death vs exit-hook vs node-cleanup

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

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

README for death

Node.js - death

Gracefully cleanup when termination signals are sent to your process.

Why?

Because adding clean up callbacks for uncaughtException, SIGINT, and SIGTERM is annoying. Ideally, you can use this package to put your cleanup code in one place and exit gracefully if you need to.

Operating System Compatibility

It's only been tested on POSIX compatible systems. Here's a nice discussion on Windows signals, apparently, this has been fixed/mapped.

Installation

npm install death

Example

var ON_DEATH = require('death'); //this is intentionally ugly

ON_DEATH(function(signal, err) {
  //clean up code here
})

Usage

By default, it sets the callback on SIGINT, SIGQUIT, and SIGTERM.

Signals

  • SIGINT: Sent from CTRL-C
  • SIGQUIT: Sent from keyboard quit action.
  • SIGTERM: Sent from operating system kill.

More discussion and detail: http://www.gnu.org/software/libc/manual/html_node/Termination-Signals.html and http://pubs.opengroup.org/onlinepubs/009695399/basedefs/signal.h.html and http://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap11.html.

AS they pertain to Node.js: http://dailyjs.com/2012/03/15/unix-node-signals/

Want to catch uncaughtException?

No problem, do this:

var ON_DEATH = require('death')({uncaughtException: true}) 

Want to know which signals are being caught?

Do this:

var ON_DEATH = require('death')({debug: true})

Your process will then log anytime it catches these signals.

Want to catch SIGHUP?

Be careful with this one though. Typically this is fired if your SSH connection dies, but can also be fired if the program is made a daemon.

Do this:

var ON_DEATH = require('death')({SIGHUP: true})

Why choose the ugly "ON_DEATH"?

Name it whatever you want. I like ON_DEATH because it stands out like a sore thumb in my code.

Want to remove event handlers?

If you want to remove event handlers ON_DEATH returns a function for cleaning up after itself:

var ON_DEATH = require('death')
var OFF_DEATH = ON_DEATH(function(signal, err) {
  //clean up code here
})

// later on...
OFF_DEATH();

License

(MIT License)

Copyright 2012, JP Richardson jprichardson@gmail.com