Introduction
Since Node.js 15, unhandled promise rejections cause the process to exit with a non-zero code. Any promise that rejects without a .catch() handler will trigger an "UnhandledPromiseRejectionWarning" followed by process termination. This is a breaking change from earlier versions where unhandled rejections only emitted a warning.
This error is common in applications with async/await code where try-catch blocks are missing or where fire-and-forget promises are used without error handling.
Symptoms
- Process exits with "UnhandledPromiseRejectionWarning" followed by crash
- Error shows the rejection reason but no stack trace pointing to your code
- Application works in development (with warning) but crashes in production
Common Causes
- Async function throws but the calling code does not use try-catch
- Promise created without .catch() handler: someAsync().then(handle)
- Event handler returns a promise that rejects, and the EventEmitter doesn't handle it
Step-by-Step Fix
- 1.Wrap async route handlers in try-catch: Handle errors in every async function.
- 2.```javascript
- 3.const express = require('express');
- 4.const router = express.Router();
// BAD: unhandled rejection if getUser or save fails router.get('/user/:id', async (req, res) => { const user = await getUser(req.params.id); res.json(user); });
// GOOD: errors are caught and handled router.get('/user/:id', async (req, res, next) => { try { const user = await getUser(req.params.id); res.json(user); } catch (err) { next(err); // Pass to Express error handler } });
// Or use async-handler wrapper: const asyncHandler = (fn) => (req, res, next) => { Promise.resolve(fn(req, res, next)).catch(next); };
router.get('/user/:id', asyncHandler(async (req, res) => { const user = await getUser(req.params.id); res.json(user); })); ```
- 1.Add global unhandled rejection handler as safety net: Catch any rejections that slip through.
- 2.```javascript
- 3.process.on('unhandledRejection', (reason, promise) => {
- 4.console.error('Unhandled Rejection at:', promise);
- 5.console.error('Reason:', reason);
- 6.// Log to error tracking service (Sentry, etc.)
- 7.// Do NOT swallow the error - let the process exit
- 8.});
process.on('uncaughtException', (error) => { console.error('Uncaught Exception:', error); // Graceful shutdown process.exit(1); }); ```
- 1.Handle errors in fire-and-forget promises: Use .catch() on standalone promises.
- 2.```javascript
- 3.// BAD: rejection is unhandled
- 4.sendEmail(user.email, 'Welcome!');
// GOOD: catch errors even for fire-and-forget sendEmail(user.email, 'Welcome!') .catch((err) => { console.error('Failed to send welcome email:', err); // Log but don't crash - this is a background task });
// Or use async IIFE: (async () => { try { await sendEmail(user.email, 'Welcome!'); } catch (err) { console.error('Failed to send welcome email:', err); } })(); ```
Prevention
- Always use try-catch around await calls
- Use express-async-errors or express-async-handler for Express routes
- Add global unhandledRejection listener for logging (but don't suppress the crash)
- Run ESLint with promise/catch-or-return rule to catch missing handlers