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. 1.Wrap async route handlers in try-catch: Handle errors in every async function.
  2. 2.```javascript
  3. 3.const express = require('express');
  4. 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. 1.Add global unhandled rejection handler as safety net: Catch any rejections that slip through.
  2. 2.```javascript
  3. 3.process.on('unhandledRejection', (reason, promise) => {
  4. 4.console.error('Unhandled Rejection at:', promise);
  5. 5.console.error('Reason:', reason);
  6. 6.// Log to error tracking service (Sentry, etc.)
  7. 7.// Do NOT swallow the error - let the process exit
  8. 8.});

process.on('uncaughtException', (error) => { console.error('Uncaught Exception:', error); // Graceful shutdown process.exit(1); }); ```

  1. 1.Handle errors in fire-and-forget promises: Use .catch() on standalone promises.
  2. 2.```javascript
  3. 3.// BAD: rejection is unhandled
  4. 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