Exit a Node.js program cleanly
Set an exit code without discarding pending work, close resources deliberately, and reserve process.exit for an intentional forced termination.
Set process.exitCode when you want to report success or failure and let Node finish normally. Call process.exit(code) only when immediate termination is intentional, because pending asynchronous work may not complete.
Report failure without forcing an exit
if (process.argv.length < 3) {
console.error('Usage: node script.cjs <argument>');
process.exitCode = 1;
} else {
console.log(`Received: ${process.argv[2]}`);
}
Setting the exit code does not stop the current function. Use branches or return from your main function so unwanted work does not continue. Zero conventionally means success; nonzero indicates failure. Document any more specific codes your CLI exposes.
Understand forced termination
process.exit(1) asks Node to terminate synchronously, even when work remains. For example, this can lose output depending on the destination:
console.log('An important final message');
process.exit(0);
Console and process-output writes are not uniformly asynchronous or synchronous across platforms and destinations. That variability is the reason not to assume the message has been fully written before forcing an exit.
Without a forced exit, Node normally exits when no referenced work keeps its event loop alive. A pending Promise alone does not guarantee that the process remains running; unreferenced handles do not keep it alive either.
Close a server deliberately
For an existing HTTP server, use an application-provided closeApplicationResources() function that returns a Promise and resolves only after its database pools, workers, and other resources have closed:
let stopping = false;
process.on('SIGTERM', () => {
if (stopping) return;
stopping = true;
const deadline = setTimeout(() => process.exit(1), 10000);
deadline.unref();
server.close(async (error) => {
try {
if (error) throw error;
await closeApplicationResources();
clearTimeout(deadline);
} catch (cleanupError) {
console.error(cleanupError.message);
process.exitCode = 1;
// Keep the deadline active if resources still hold the process open.
}
});
});
server.close() stops accepting new connections and waits for the connections covered by its lifecycle rules. Idle-connection handling differs across Node releases; upgraded connections such as WebSockets need their own shutdown logic. Add application-specific cleanup and a suitable deadline for your deployment. The callback is not proof that every database pool or worker has also closed.
Keep the deadline until all required cleanup completes. Because it is unreferenced, it will not keep an otherwise finished process alive. A timer also cannot interrupt JavaScript that blocks the event loop. The timeout is a final forced-failure policy, not a substitute for cleanup. Signal handling also differs by operating system; test the process manager and shutdown path you deploy.
FAQ
Which exit codes should I use? Zero is success and nonzero is failure. Code 1 commonly means general failure, while code 2 often indicates usage problems, but those meanings are conventions. Shells commonly represent signal termination with 128 + signal number; it is not a universal application-code allocation rule.
Why does the process stay alive? Look for listening servers, timers, sockets, worker threads, or open client pools. process.getActiveResourcesInfo() can help identify resource types on releases that provide it. Avoid depending on the private _getActiveHandles() API as a stable interface.
Can I clean up asynchronously in the exit event? No. Exit listeners must perform synchronous work. Start asynchronous cleanup earlier, when receiving a shutdown signal or deciding to end the application. The beforeExit event also has distinct semantics and is not emitted for every termination path.
References
Contact us
Join the community
to join the community
Try Guance
Start online and pay only for what you use.
Get startedChoose a Guance plan