This warning occurs when a Promise is rejected but no error handler catches the rejection. Starting from Node.js 15, unhandled promise rejections cause the application to terminate, making proper error handling critical for application stability.
The UnhandledPromiseRejectionWarning is emitted by the Node.js runtime when a Promise is rejected but no rejection handler (like .catch() or try/catch) is attached to handle the error. This indicates a gap in error handling that can lead to silent failures or, in Node.js 15 and later versions, application termination. When JavaScript executes asynchronous code using Promises or async/await, errors must be explicitly caught. Unlike synchronous code where uncaught exceptions bubble up, rejected Promises require explicit error handlers. Without them, the error remains "unhandled" and Node.js emits this warning to alert developers of the potential problem. In earlier versions of Node.js (before v15), unhandled rejections would only log a warning but allow the application to continue. However, this behavior changed in Node.js 15 where unhandled rejections now cause the process to exit with a non-zero status code, making this error critical to address in production environments.
Ensure every Promise chain has a .catch() handler at the end:
// Before (causes warning)
fetchData()
.then(data => processData(data))
.then(result => console.log(result));
// After (handles errors)
fetchData()
.then(data => processData(data))
.then(result => console.log(result))
.catch(error => {
console.error('Error processing data:', error);
// Handle error appropriately
});This catches any error that occurs at any point in the Promise chain.
For async/await syntax, always use try/catch:
// Before (causes warning)
async function getData() {
const data = await fetchData();
const result = await processData(data);
return result;
}
// After (handles errors)
async function getData() {
try {
const data = await fetchData();
const result = await processData(data);
return result;
} catch (error) {
console.error('Error in getData:', error);
throw error; // Re-throw or handle as needed
}
}When calling async functions, handle their potential rejections:
// Before (causes warning)
async function main() {
await getData();
}
main(); // No error handling!
// After (handles errors)
async function main() {
try {
await getData();
} catch (error) {
console.error('Error in main:', error);
process.exit(1);
}
}
// Or with .catch()
main().catch(error => {
console.error('Error in main:', error);
process.exit(1);
});Add a process-level handler as a safety net (not a replacement for proper error handling):
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
// Log to error monitoring service
// DO NOT use this as primary error handling
// Consider exiting the process:
process.exit(1);
});This catches any rejections that slip through, but proper error handling in your code is still essential.
Configure ESLint with rules to catch unhandled Promises during development:
npm install --save-dev eslint @typescript-eslint/eslint-pluginAdd to your .eslintrc.json:
{
"rules": {
"no-floating-promises": "error",
"@typescript-eslint/no-floating-promises": "error",
"promise/catch-or-return": "error"
},
"plugins": ["promise", "@typescript-eslint"]
}This prevents unhandled Promises from reaching production.
Promise Constructor Anti-pattern: Avoid creating Promises manually unless necessary. If you do, ensure both resolve and reject paths are handled:
// Be careful with Promise constructor
new Promise((resolve, reject) => {
someAsyncOperation((err, result) => {
if (err) return reject(err); // Must handle this rejection
resolve(result);
});
}).catch(error => console.error(error)); // Don't forget thisNode.js Version Behavior: The handling of unhandled rejections has evolved:
- Node.js <15: Warning only, process continues
- Node.js 15+: Process terminates by default
- Use --unhandled-rejections=warn flag to restore old behavior (not recommended)
Error in Error Handlers: A common subtle issue is errors occurring within catch blocks themselves:
fetchData()
.catch(error => {
// If this JSON.parse fails, it creates another unhandled rejection
const parsed = JSON.parse(error.message);
});
// Solution: wrap error handling logic
fetchData()
.catch(error => {
try {
const parsed = JSON.parse(error.message);
} catch (parseError) {
console.error('Error parsing error message:', parseError);
}
});Async Event Handlers: Event emitters with async listeners need special care:
// Problematic
emitter.on('data', async (data) => {
await processData(data); // Unhandled if this rejects
});
// Better
emitter.on('data', async (data) => {
try {
await processData(data);
} catch (error) {
console.error('Error processing data:', error);
}
});Testing: Use tools like node --trace-warnings to get full stack traces showing where unhandled rejections originate, making them easier to debug.
Error: EMFILE: too many open files, watch
EMFILE: fs.watch() limit exceeded
Error: Middleware next() called multiple times (next() invoked twice)
Express middleware next() called multiple times
Error: Worker failed to initialize (worker startup error)
Worker failed to initialize in Node.js
Error: EMFILE: too many open files, open 'file.txt'
EMFILE: too many open files
Error: cluster.fork() failed (cannot create child process)
cluster.fork() failed - Cannot create child process