We spend hours perfecting our CI/CD pipelines, optimizing database queries, and writing clean code. But we often ignore one crucial part of the application lifecycle: Death.
When you deploy a new version of your app to Kubernetes, Docker Swarm, or even PM2, the orchestrator sends a SIGTERM signal to your running process.
If you don't handle this signal, Node.js might terminate immediately. Active HTTP requests are severed, database connections remain open (ghost connections), and transactions might be left in limbo.
Today, I want to show you how to handle this professionally using ds-express-errors, specifically diving deep into its Graceful Shutdown API.
The "Zero-Dependency" Approach
I built ds-express-errors primarily for error mapping, but I included a robust shutdown manager because I was tired of pulling in extra dependencies just to catch a SIGINT.
The library has 0 dependencies, meaning it's lightweight and secure.
The Full Configuration (Not Just the Basics)
Most tutorials only show you how to close the server. But real production apps need more: they need to distinguish between a "Planned Shutdown" (Deployment) and a "Crash" (Uncaught Exception), and they need granular control over exit behavior.
Here is the complete API reference for initGlobalHandlers.
The Setup
First, install the package:
npm install ds-express-errors
Now, let's look at the "Power User" configuration in your entry file (e.g., index.js):
const express = require('express');
const mongoose = require('mongoose');
const { initGlobalHandlers, gracefulHttpClose } = require('ds-express-errors');
const app = express();
const server = app.listen(3000);
// The Complete Configuration
initGlobalHandlers({
// 1. The HTTP Draining Mechanism
// Wraps server.close() in a promise that waits for active requests to finish.
closeServer: gracefulHttpClose(server),
// 2. Normal Shutdown Logic (SIGINT, SIGTERM)
// This runs when you redeploy or stop the server manually.
onShutdown: async () => {
console.log('SIGTERM received. Closing external connections...');
// Close DB, Redis, Socket.io, etc.
await mongoose.disconnect();
console.log('Cleanup finished. Exiting.');
},
// 3. Crash Handling (Uncaught Exceptions / Unhandled Rejections)
// This runs when your code throws an error you didn't catch.
onCrash: async (error) => {
console.error('CRITICAL ERROR:', error);
// Critical: Send alert to Sentry/Slack/PagerDuty immediately
// await sendAlert(error);
},
// 4. Exit Strategy for Unhandled Rejections
// Default: true.
// If false, the process continues running even after an unhandled promise rejection.
// (Recommended: true, because an unhandled rejection can leave the app in an unstable state)
exitOnUnhandledRejection: true,
// 5. Exit Strategy for Uncaught Exceptions
// Default: true.
// If false, the app tries to stay alive after a sync error.
// (High risk of memory leaks or corrupted state if set to false).
exitOnUncaughtException: true
});
Breakdown of the Options
Let's dissect what we just wrote. The initGlobalHandlers function accepts an options object with these specific keys:
closeServer
Type: Function (Async)
This is where you handle the HTTP layer. The library provides a helper gracefulHttpClose(server) which listens for the abort signal and handles the callback hell of the native Node.js server.close(). It ensures no new connections are accepted, but keeps the process alive until existing users finish their requests.
onShutdown
Type: Function (Async)
This is for your business logic cleanup. It is triggered only by system signals (SIGINT, SIGTERM, SIGQUIT).
This is the "Happy Path" of death. You should:
- Close Database connections.
- Flush logs.
- Cancel long-running jobs.
onCrash
Type: Function (Async)
This is unique. Many libraries treat a crash the same as a shutdown. But ds-express-errors separates them. onCrash is triggered by uncaughtException or unhandledRejection.
Why separate them?
In onShutdown, you are calm. In onCrash, the house is on fire. You might not want to wait for a graceful DB disconnect; you might want to fire a "HELP ME" alert to Slack and die immediately.
exitOnUnhandledRejection & exitOnUncaughtException
Type: Boolean (Default: true)
By default, the library follows Node.js best practices: fail fast. If the process state is corrupted (unhandled error), it logs the error, runs onCrash, and exits with code 1.
However, if you have a specific requirement to keep the process alive (perhaps you have an external supervisor or a very specific resilience strategy), you can toggle these to false.
The Safety Net: Timeouts
What if your onShutdown logic hangs? What if mongoose.disconnect() never resolves?
You don't want your pod to hang in the "Terminating" state forever (Kubernetes will eventually SIGKILL it anyway).
ds-express-errors wraps your shutdown logic in a 10-second timeout. If your cleanup functions don't resolve by then, it forces an exit to prevent zombie processes.
Conclusion
Handling process termination is part of being a senior engineer. It distinguishes a "hobby project" from a reliable distributed system.
With ds-express-errors, you don't need complex boilerplate. You get a fully typed, zero-dependency solution that handles both graceful shutdowns and critical crashes out of the box
.
Links:
Happy coding (and happy shutting down)! 🔌
Top comments (0)