Event handler that fires when a Node.js process exits regardless of how termination occurs.
npx @tessl/cli install tessl/npm-signal-exit@4.1.0Signal Exit provides a reliable way to execute cleanup code when a Node.js process exits, regardless of how the termination occurs - whether through normal completion, explicit process.exit() calls, signal handling, or fatal external signals.
npm install signal-exitimport { onExit } from "signal-exit";For CommonJS:
const { onExit } = require("signal-exit");For signals array:
import { signals } from "signal-exit/signals";For browser environments:
import { onExit } from "signal-exit/browser";import { onExit } from "signal-exit";
// Register an exit handler
const removeHandler = onExit((code, signal) => {
console.log(`Process exiting with code: ${code}, signal: ${signal}`);
// Perform cleanup operations here
});
// Remove the handler if needed
// removeHandler();Signal Exit is built around several key components that work together to provide reliable exit handling:
process.emit and process.reallyExit to intercept all exit scenariosThe library uses a singleton pattern to ensure only one instance manages exit handling per process, with automatic cleanup and re-registration capabilities for complex scenarios.
Register a function to be called when the process exits for any reason.
/**
* Called when the process is exiting, whether via signal, explicit
* exit, or running out of stuff to do.
*
* If the global process object is not suitable for instrumentation,
* then this will be a no-op.
*
* Returns a function that may be used to unload signal-exit.
*/
function onExit(
cb: Handler,
opts?: { alwaysLast?: boolean }
): () => void;
/**
* A function that takes an exit code and signal as arguments
*
* In the case of signal exits *only*, a return value of true
* will indicate that the signal is being handled, and we should
* not synthetically exit with the signal we received. Regardless
* of the handler return value, the handler is unloaded when an
* otherwise fatal signal is received, so you get exactly 1 shot
* at it, unless you add another onExit handler at that point.
*
* In the case of numeric code exits, we may already have committed
* to exiting the process, for example via a fatal exception or
* unhandled promise rejection, so it is impossible to stop safely.
*/
type Handler = (
code: number | null | undefined,
signal: NodeJS.Signals | null
) => true | void;Usage Examples:
import { onExit } from "signal-exit";
// Basic cleanup handler
onExit((code, signal) => {
console.log("Cleaning up resources...");
// Close database connections, save files, etc.
});
// Handler that runs after all other handlers
onExit((code, signal) => {
console.log("Final cleanup step");
}, { alwaysLast: true });
// Signal capture (prevent synthetic exit)
onExit((code, signal) => {
if (signal === 'SIGTERM') {
console.log("Graceful shutdown initiated");
// Perform graceful shutdown
return true; // Prevent synthetic process.kill
}
});Access the array of signals that trigger exit handlers.
/**
* Platform-specific array of signals that can trigger exit handlers.
* Contains signals like SIGHUP, SIGINT, SIGTERM, and others based on the platform.
*/
const signals: NodeJS.Signals[];Usage Examples:
import { signals } from "signal-exit/signals";
console.log("Monitored signals:", signals);
// On Linux: ['SIGHUP', 'SIGINT', 'SIGTERM', 'SIGALRM', 'SIGABRT', ...]
// On Windows: ['SIGHUP', 'SIGINT', 'SIGTERM']Advanced functions for controlling the signal exit machinery (primarily for testing).
/**
* Load the listeners. Likely you never need to call this, unless
* doing a rather deep integration with signal-exit functionality.
* Mostly exposed for the benefit of testing.
*
* @internal
*/
function load(): void;
/**
* Unload the listeners. Likely you never need to call this, unless
* doing a rather deep integration with signal-exit functionality.
* Mostly exposed for the benefit of testing.
*
* @internal
*/
function unload(): void;No-op implementations for browser environments where process signals don't exist.
/**
* Browser-compatible version that provides the same interface
* but performs no operations (no-op functions).
*/
const onExit: (
cb: Handler,
opts: { alwaysLast?: boolean }
) => () => void;
const load: () => void;
const unload: () => void;Usage Examples:
import { onExit } from "signal-exit/browser";
// Works in browser but does nothing
const remove = onExit((code, signal) => {
console.log("This won't be called in browser");
});import { onExit } from "signal-exit";
onExit((code, signal) => {
if (signal) {
console.log(`Received signal: ${signal}`);
// Handle different signals differently
switch (signal) {
case 'SIGTERM':
console.log("Graceful shutdown requested");
performGracefulShutdown();
return true; // Prevent synthetic kill
case 'SIGINT':
console.log("Interrupt signal received");
cleanup();
break;
default:
console.log("Other signal received");
cleanup();
}
} else {
console.log(`Process exiting with code: ${code}`);
cleanup();
}
});import { onExit } from "signal-exit";
// This runs first
const remove1 = onExit((code, signal) => {
console.log("First handler - emergency cleanup");
emergencyCleanup();
});
// This runs second
const remove2 = onExit((code, signal) => {
console.log("Second handler - regular cleanup");
regularCleanup();
});
// This runs last (after all other handlers)
const remove3 = onExit((code, signal) => {
console.log("Final handler - logging and reporting");
logShutdown(code, signal);
}, { alwaysLast: true });import { onExit } from "signal-exit";
class Application {
private exitHandler?: () => void;
start() {
this.exitHandler = onExit((code, signal) => {
this.cleanup();
});
}
stop() {
// Remove the exit handler when no longer needed
if (this.exitHandler) {
this.exitHandler();
this.exitHandler = undefined;
}
}
cleanup() {
console.log("Cleaning up application resources");
}
}The library is designed to be robust and handle edge cases:
process object is unavailable, onExit returns a no-op function