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.
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.
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;
});
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;
});
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
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
});
You are building a CLI that creates temporary files. You need to delete them on exit.
exit-hookconst exitHook = require('exit-hook');
const fs = require('fs');
exitHook(() => {
fs.unlinkSync('/tmp/temp-file');
});
You run a server that needs to close DB connections gracefully on SIGTERM.
node-cleanupconst nodeCleanup = require('node-cleanup');
nodeCleanup((exitCode, signal) => {
console.log(`Shutting down: ${signal}`);
db.close();
return false;
});
You are maintaining an old script that already uses death.
death (temporarily)exit-hook later.// Legacy code
const death = require('death');
death(() => { /* ... */ });
| Feature | death | exit-hook | node-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 |
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.
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.
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.
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.
Gracefully cleanup when termination signals are sent to your process.
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.
It's only been tested on POSIX compatible systems. Here's a nice discussion on Windows signals, apparently, this has been fixed/mapped.
npm install death
var ON_DEATH = require('death'); //this is intentionally ugly
ON_DEATH(function(signal, err) {
//clean up code here
})
By default, it sets the callback on SIGINT, SIGQUIT, and SIGTERM.
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/
No problem, do this:
var ON_DEATH = require('death')({uncaughtException: true})
Do this:
var ON_DEATH = require('death')({debug: true})
Your process will then log anytime it catches these signals.
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})
Name it whatever you want. I like ON_DEATH because it stands out like a sore thumb in my code.
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();
(MIT License)
Copyright 2012, JP Richardson jprichardson@gmail.com