const SHUTDOWN_TIMEOUT_MS = 10_000;
function registerFatalHandlers({ logger, onFatal, exitCode = 1 }) {
let shuttingDown = false;
async function handleFatal(kind, error) {
if (shuttingDown) {
logger.error({ kind }, 'fatal error during shutdown, forcing exit');
process.exit(exitCode);
}
shuttingDown = true;
logger.fatal({ kind, err: error }, 'fatal error, shutting down');
const watchdog = setTimeout(() => {
logger.error('shutdown timed out, forcing exit');
process.exit(exitCode);
}, SHUTDOWN_TIMEOUT_MS);
watchdog.unref();
try {
await onFatal(error);
process.exit(exitCode);
} catch (cleanupErr) {
logger.error({ err: cleanupErr }, 'error during graceful shutdown');
process.exit(exitCode);
}
}
process.on('uncaughtException', (err) => {
handleFatal('uncaughtException', err);
});
process.on('unhandledRejection', (reason) => {
const err = reason instanceof Error ? reason : new Error(String(reason));
handleFatal('unhandledRejection', err);
});
}
module.exports = { registerFatalHandlers, SHUTDOWN_TIMEOUT_MS };
const http = require('http');
function trackConnections(server) {
const sockets = new Set();
server.on('connection', (socket) => {
sockets.add(socket);
socket.once('close', () => sockets.delete(socket));
});
return sockets;
}
function createServer(app, { logger }) {
const server = http.createServer(app);
const sockets = trackConnections(server);
function listen(port) {
return new Promise((resolve) => {
server.listen(port, () => {
logger.info({ port }, 'server listening');
resolve(server);
});
});
}
function shutdown() {
return new Promise((resolve) => {
logger.info('closing server, draining connections');
server.close(() => {
logger.info('all connections drained');
resolve();
});
setTimeout(() => {
for (const socket of sockets) socket.destroy();
}, 5_000).unref();
});
}
return { listen, shutdown };
}
module.exports = { createServer };
const { createServer } = require('./server');
const { registerFatalHandlers } = require('./fatal');
const { buildApp } = require('./app');
const logger = require('./logger');
async function main() {
const app = buildApp({ logger });
const server = createServer(app, { logger });
registerFatalHandlers({
logger,
exitCode: 1,
onFatal: () => server.shutdown(),
});
for (const signal of ['SIGTERM', 'SIGINT']) {
process.once(signal, async () => {
logger.info({ signal }, 'signal received, shutting down');
await server.shutdown();
process.exit(0);
});
}
await server.listen(process.env.PORT || 3000);
}
main().catch((err) => {
logger.fatal({ err }, 'failed to start');
process.exit(1);
});
This snippet shows the standard pattern for handling fatal, out-of-band errors in a Node.js process: uncaughtException and unhandledRejection. The key insight is that once an exception escapes to uncaughtException, the process is in an undefined state — some part of the event loop threw and nobody caught it, so continuing to serve traffic risks corrupted state or leaked resources. The correct move is to log the error with full context and then exit, letting a supervisor (systemd, Kubernetes, PM2) restart a clean process.
The fatal handlers tab installs both listeners exactly once via registerFatalHandlers. It guards against re-entrancy with a shuttingDown flag so a second error during cleanup can't trigger a second overlapping shutdown. On unhandledRejection it wraps non-Error reasons so downstream logging always sees a real stack. It calls a caller-supplied onFatal (typically the server's shutdown) with a hard SHUTDOWN_TIMEOUT_MS watchdog: if graceful cleanup hangs, setTimeout(...).unref() fires and forces process.exit(1). The .unref() matters — it stops the timer itself from keeping the event loop alive.
The http server tab wires this into a real service. createServer returns a shutdown function that stops accepting new connections via server.close(), then waits for in-flight requests to drain before resolving. trackConnections keeps a Set of open sockets so a slow client can be destroyed after the drain window. The same shutdown is reused for SIGTERM and SIGINT, giving one code path for both operator-initiated and crash-initiated exits — an important consistency, since duplicating shutdown logic is a common source of resource leaks.
The bootstrap tab is the composition root: it builds the app, starts the server, and passes server.shutdown into registerFatalHandlers with the exit code. The distinction between unhandledRejection and uncaughtException deserves care: promise rejections are often recoverable application bugs, and some teams choose to log-and-continue there. This implementation treats both as fatal for safety, favoring a fast restart over a zombie process. A subtle pitfall avoided here is doing async work inside the handler without a timeout — without the watchdog, a stuck flush would hang the process forever and defeat the restart. Reach for this pattern in any long-running Node service where correctness after an unexpected throw cannot be guaranteed.
Related snips
package api
import (
"net/http"
"runtime/debug"
)
Expose build metadata for debugging deploys
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
class Product(models.Model):
name = models.CharField(max_length=200)
slug = models.SlugField(blank=True)
price = models.DecimalField(max_digits=10, decimal_places=2)
cost = models.DecimalField(max_digits=10, decimal_places=2)
margin = models.DecimalField(max_digits=5, decimal_places=2, blank=True)
Django model signals vs overriding save
package dbutil
import (
"context"
"github.com/jackc/pgconn"
Retry Postgres serialization failures with bounded attempts
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
const success = true;
setTimeout(() => {
if (success) {
Promises and async/await patterns for asynchronous JavaScript
use tracing::{info, instrument};
#[instrument]
fn process_request(user_id: u64) {
info!(user_id, "Processing request");
// Work happens here
tracing for structured logging and distributed tracing
Share this code
Here's the card — post it anywhere.