What this usually means
An UnhandledPromiseRejection means a Promise has rejected (thrown an error) but no code caught it—no .catch() or try/catch in an async function. In Node.js v15+, this leads to process termination; in older versions, you’ll see warnings instead (often ignored until something breaks). It usually occurs when async errors aren’t surfaced, especially when returning promises from handlers or inside event emitters. These are easy to miss and can cause elusive production bugs.
The first ten minutes — establish facts before touching code.
- 1Search logs for 'UnhandledPromiseRejectionWarning': grep -i 'UnhandledPromiseRejection' app.log
- 2Reproduce your stack with NODE_ENV=development to ensure all warnings display.
- 3Add process.on('unhandledRejection', ...) handler to log stack traces in detail.
- 4Identify which Promise chain is missing a catch() by correlating error messages and call sites.
- 5Temporarily add global.catch handlers to all top-level async functions for rapid isolation.
The specific files, logs, configs, and dashboards that usually own this bug.
- searchApplication logs (app.log, stdout, stderr) at the time of the warning.
- searchStack traces printed by the UnhandledPromiseRejectionWarning.
- searchThe specific async function or promise chain that triggered the error.
- searchUnhandled rejection hook: process.on('unhandledRejection', ...)
- searchSources of asynchronous calls (DB queries, API requests, file reads).
- searchCI/CD pipeline logs if this surfaces during test execution.
- searchPlaces that use async/await without try/catch blocks.
Practical causes, not theory. These are the things you will actually find.
- warningAsync route/controller returned a promise but no error handler was attached.
- warningForgotten .catch() in a chain of Promises (e.g., userService.load().then(...)).
- warningThrown error inside an async function not wrapped in try/catch.
- warningEventEmitter listener using async handler without error handling.
- warningDatabase or network failure not handled in an awaited call.
- warningUpgraded Node.js version—default behavior now kills process on unhandled rejection.
- warningThird-party library returns rejected promise outside your error handling.
Concrete fix directions. Pick the one that matches your root cause.
- buildWrap every async function body with try/catch and handle errors explicitly.
- buildAlways return or await Promise chains and attach .catch() at the end.
- buildAdd process.on('unhandledRejection', ...) at process startup for visibility.
- buildCheck all Express/Koa routes for missing await and missing error middleware.
- buildRefactor event listeners to handle errors from async handlers.
- buildSet 'unhandled-rejections=strict' in Node.js to catch issues early in development.
A fix you cannot prove is a guess. Close the loop.
- verifiedRun NODE_ENV=production node --unhandled-rejections=strict app.js and verify app stays up under error conditions.
- verifiedCheck that logs no longer include UnhandledPromiseRejectionWarning after fixes.
- verifiedWrite integration tests that intentionally cause promise rejections and verify graceful error responses.
- verifiedSimulate DB/network failures in development and confirm errors are properly caught.
- verifiedRun node with --trace-warnings and confirm no stack traces are missed.
Things that make this bug worse or harder to find.
- warningIgnoring UnhandledPromiseRejection warnings—they will crash Node.js in modern versions.
- warningAdding a global unhandledRejection handler that just logs without fixing upstream code.
- warningSwallowing errors without logging (e.g., empty catch blocks).
- warningWrapping non-async code with try/catch and assuming it handles promise errors.
- warningRelying on .catch() only in some Promise chains but missing top-level handlers.
- warningFailing to update older patterns after a Node.js version upgrade.
Express API Randomly Crashes After Deploy
Timeline
- 10:03PagerDuty alert: API container stopped unexpectedly.
- 10:05Ops confirms process exited with code 1, no obvious out-of-memory.
- 10:08Developer checks CloudWatch logs, finds (node:430) UnhandledPromiseRejectionWarning.
- 10:11Search traces error to userController.js: forgot to await db.saveUser().
- 10:18Reproduce locally: error only appears with malformed POST body.
- 10:23Patch: wrap async handler in try/catch and ensure .catch() on all promise chains.
- 10:28Deploy hotfix; process stays up, errors get logged properly.
I got paged when our Node.js API started crashing a few minutes after a minor code deploy. Logs showed process exit code 1, but there was no stack trace on standard error—just a generic process shutdown.
Digging into CloudWatch, I found the telltale (node:430) UnhandledPromiseRejectionWarning. Turns out, in userController.js, someone added a db.saveUser() without await or .catch(). With Node.js 16, any uncaught promise rejection kills the process.
After patching all async handlers to wrap with try/catch, and running the app locally with --unhandled-rejections=strict, we confirmed there were no more uncaught rejections. It was a classic missed error path in a rarely used endpoint that only showed up in production.
Root cause
Async handler in Express route did not await or catch a rejected promise, causing unhandled rejection and process crash.
The fix
Added await and try/catch to all async controller functions; ensured rejected promises were never left uncaught.
The lesson
With Node.js 15+, unhandled rejections aren’t warnings—they’re fatal. Always handle async errors explicitly.
Prior to Node.js v15, unhandled promise rejections would print a warning but let the app live, making it easy to ignore root causes. From v15 onward, the default is to terminate the process with a non-zero exit code.
You can control this behavior with node --unhandled-rejections=mode, but best practice is to never have unhandled rejections at all. Always address the actual missing error handling during development, or the app might crash in production with no clear cause.
Typical triggers include forgetting to await a promise in an async function or omitting .catch() on a promise chain. Event-based systems make this worse, as event handlers (especially in Express or Koa) often return promises that aren’t handled.
Review all custom EventEmitter handlers and third-party library integrations, as these are frequent sources of silent, uncaught rejections. Wrap all top-level async code with robust try/catch blocks.
Adding a process.on('unhandledRejection', ...) is non-negotiable during development. Print both the reason and the full promise stack. Use node --trace-warnings to capture all stack traces.
Instrument integration tests to simulate error conditions (timeout, thrown error, bad network) and verify that every rejection is surfaced and logged, not silently swallowed.
Automate detection in CI by running tests with --unhandled-rejections=strict. Mandate code review policies that require error handling for every async/await function.
Consider linting rules or TypeScript compiler configs that flag missing try/catch or catch() on promises, especially in handler functions.
Frequently asked questions
Why did my Node.js process crash on an unhandled promise rejection?
Node.js v15 and newer terminate the process by default when a promise rejection is unhandled. This prevents silent bugs but can crash your app if you miss a catch.
Is process.on('unhandledRejection') a fix?
It’s a diagnostic tool, not a fix. Use it to log and debug the exact site of missed error handling, but always address the root cause in the application code.
How do I find which promise wasn't caught?
Check the stack trace in the UnhandledPromiseRejectionWarning. Add more context in your process.on('unhandledRejection') handler to log the promise and error details.
Can I safely ignore unhandled promise rejection warnings in dev?
No—you’re just delaying a production outage. Modern Node.js treats them as fatal errors, so you should always fix the underlying code before shipping.
What’s the best way to prevent this in a large codebase?
Standardize async error handling patterns, enforce linting, and run tests with --unhandled-rejections=strict so new unhandled rejections are detected before deploy.