Handling Uncaught Exceptions and Promise Rejections in Node.js
In Node.js applications, failing to handle asynchronous errors and unexpected runtime exceptions can lead to silent failures, memory leaks, or abrupt application crashes. This article provides a straightforward guide on how to detect, log, and gracefully handle unhandled promise rejections and uncaught exceptions in Node.js, ensuring your application remains stable and resilient.
Uncaught Exceptions
An uncaught exception occurs when a synchronous error is thrown
during the execution of your code but is not caught by any
try...catch block.
To intercept these errors globally, you listen to the
uncaughtException event on the global process
object.
process.on('uncaughtException', (error) => {
console.error('There was an uncaught error', error);
// Perform cleanup operations here (e.g., closing database connections)
process.exit(1); // Mandatory retry/restart
});Crucial Best Practice: When an uncaught exception
occurs, the application is in an undefined state. You must log the
error, perform any necessary cleanup, and exit the process using
process.exit(1). Use a process manager like PM2, Docker, or
Kubernetes to automatically restart the application container.
Unhandled Promise Rejections
An unhandled promise rejection occurs when an asynchronous operation
(represented by a Promise) fails, but there is no .catch()
block or try...catch block around the await
statement to handle the rejection.
To intercept these rejections globally, listen to the
unhandledRejection event on the process
object.
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
// Application specific logging, throwing an error, or cleanup here
});Unlike uncaught exceptions, you do not always need to crash the process immediately on an unhandled rejection, although modern versions of Node.js (v15+) will terminate the process by default with a non-zero exit code if these are not handled.
Recommended Implementation Strategy
To handle both scenarios effectively in a production environment,
implement a centralized error-handling script that initializes at the
very start of your application entry point (e.g., index.js
or server.js).
const logger = require('./logger'); // Your logging library
// Handle synchronous uncaught exceptions
process.on('uncaughtException', (error) => {
logger.error(`Uncaught Exception: ${error.message}`, { stack: error.stack });
// Graceful shutdown
gracefulShutdown(1);
});
// Handle asynchronous unhandled rejections
process.on('unhandledRejection', (reason, promise) => {
logger.error('Unhandled Rejection at:', { promise, reason });
// Recommended: Treat unhandled rejections as uncaught exceptions
gracefulShutdown(1);
});
function gracefulShutdown(code) {
// 1. Close active servers (e.g., Express app)
// 2. Close database connections (e.g., MongoDB, PostgreSQL)
// 3. Exit process
setTimeout(() => {
process.exit(code);
}, 1000).unref(); // Force exit if cleanup takes too long
}