How to Catch and Handle Unhandled Promise Rejections in Node.js: A Complete Guide
Use a two-layer strategy: always attach local .catch() or try/catch blocks to promises, then register a global process.on('unhandledRejection') listener that re-throws errors to your central uncaughtException handler.
Modern Node.js applications rely heavily on promises and async/await syntax. When a promise rejects without a corresponding .catch() handler or try/catch block, the error bubbles up to the event loop and emits an unhandledRejection event. According to the goldbergyoni/nodebestpractices repository, relying solely on this warning is dangerous because your process may continue running in an undefined error state. Implementing a robust strategy to catch and handle unhandled promise rejections in Node.js ensures predictable behavior and prevents silent failures.
Why Unhandled Promise Rejections Are Dangerous
When a promise rejection goes unhandled, Node.js prints a deprecation warning to stderr, but the process keeps running by default. This creates a silent failure mode where your application might serve requests while internal state is corrupted. In production environments, this leads to memory leaks, inconsistent data, and difficult-to-debug crashes that occur hours after the original error.
The Two-Layer Strategy to Handle Unhandled Promise Rejections
The nodebestpractices documentation recommends a defensive architecture with two complementary layers: local handling for expected errors and global fallback for unexpected rejections.
Layer 1: Local Error Handling with .catch() and try/catch
Always attach error handlers at the point where promises originate. This prevents rejections from ever becoming "unhandled" and allows context-specific recovery.
// Using .catch() with promise chains
DAL.getUserById(1)
.then(user => {
if (!user.isAlive) throw new Error('User is dead');
})
.catch(err => {
// Handle the error where it originated
errorManagement.handler.handleError(err);
});
// Using async/await with try/catch
async function processUser(id) {
try {
const user = await DAL.getUserById(id);
if (!user.isAlive) throw new Error('User is dead');
} catch (err) {
errorManagement.handler.handleError(err);
}
}
Layer 2: Global Fallback with process.on('unhandledRejection')
Despite local handling, programming errors or third-party library bugs may still produce stray rejections. Register a global listener to catch these as a last resort.
Implementing the Global Safety Net
The recommended pattern in sections/errorhandling/catchunhandledpromiserejection.md converts unhandled rejections into exceptions that your existing uncaughtException handler can process. This eliminates duplicate logic and ensures consistent cleanup.
// Global fallback for unhandled promise rejections
process.on('unhandledRejection', (reason, promise) => {
// Forward to the central error handler by converting to exception
throw reason;
});
// Global handler for any uncaught synchronous errors
process.on('uncaughtException', (error) => {
errorManagement.handler.handleError(error);
if (!errorManagement.handler.isTrustedError(error)) {
// Exit the process after cleanup – avoid undefined state
process.exit(1);
}
});
Why re-throw inside unhandledRejection?
Converting the rejection to a thrown exception allows your existing uncaughtException logic to handle diagnostics, logging, and graceful shutdown from a single code path. This prevents divergence between how you handle synchronous and asynchronous fatal errors.
Local Handling Best Practices
When implementing local error handling, avoid these common anti-patterns demonstrated in the nodebestpractices examples:
Anti-pattern: Missing .catch()
// This will trigger the global unhandledRejection handler
DAL.getUserById(1).then(user => {
if (!user.isAlive) throw new Error('User is dead');
});
Pattern: Proper error attachment
Always ensure every promise chain terminates with a .catch() or is wrapped in try/catch when using await.
Key Files in the nodebestpractices Repository
The following files in goldbergyoni/nodebestpractices provide authoritative guidance on this pattern:
sections/errorhandling/catchunhandledpromiserejection.md– Core documentation explaining the two-layer strategy, including the rationale for re-throwing rejections to theuncaughtExceptionhandler.README.md– Overview linking to all error-handling best practices sections.sections/errorhandling/– Additional context for comprehensive error management strategies in Node.js applications.
Summary
- Always use local handling: Attach
.catch()to promise chains or wrapawaitcalls intry/catchblocks to handle expected errors at their source. - Implement a global fallback: Register
process.on('unhandledRejection')to catch stray rejections from programming errors or library bugs. - Re-throw to unify handling: Convert unhandled rejections to exceptions by throwing the reason inside the rejection handler, allowing your existing
uncaughtExceptionlogic to manage cleanup and shutdown. - Exit on fatal errors: When global handlers catch untrusted (unexpected) errors, gracefully shut down the process after logging to avoid running in a corrupted state.
Frequently Asked Questions
What happens if I don't handle unhandled promise rejections?
If you don't catch and handle unhandled promise rejections in Node.js, the process emits a deprecation warning but continues running by default. This creates a dangerous silent failure mode where your application may serve requests while internal state is corrupted, leading to memory leaks, data inconsistency, and delayed crashes that are difficult to diagnose.
Why should I re-throw unhandled rejections instead of handling them directly?
Re-throwing the rejection reason inside process.on('unhandledRejection') converts the asynchronous error into a synchronous exception. This allows your existing process.on('uncaughtException') handler to process both types of errors through a single code path, eliminating duplicate logic and ensuring consistent logging, diagnostics, and graceful shutdown procedures.
Is process.on('unhandledRejection') enough to catch all async errors?
No, process.on('unhandledRejection') only catches promise rejections that lack a .catch() handler. It does not catch errors thrown inside synchronous code, which are handled by uncaughtException, or operational errors that are properly caught locally. You need both local error handling (.catch() or try/catch) and global listeners to build a comprehensive defense.
How do I handle unhandled rejections in Express.js applications?
In Express.js applications, unhandled rejections in route handlers often occur when an async route lacks error handling middleware. First, ensure all async routes use a catch block or an Express wrapper that forwards errors to next(err). Second, implement the global process.on('unhandledRejection') listener that re-throws to your uncaughtException handler, ensuring any stray rejections trigger your centralized error management and graceful shutdown logic.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →