UnhandledPromiseRejectionWarning means a Promise rejected somewhere in your application and nothing was ever attached to handle that rejection — and in current Node.js versions, this isn't just a warning anymore; unhandled rejections crash the process by default, treating an ignored error the same seriousness as an uncaught synchronous exception.
This warning (or, in current Node versions, fatal error) means a Promise reached a rejected state without a corresponding .catch() handler or a try/catch around its await, anywhere in the chain — the rejection essentially had nowhere to go, which Node correctly treats as a sign your application has an unhandled error condition it doesn't actually know how to recover from.
Why This Error Happens
Every Promise that rejects needs a handler somewhere along its chain to be considered "handled" — if it doesn't have one by the time Node's event loop finishes processing microtasks, Node fires this warning (older versions) or crashes the process (Node 15+, since unhandled rejection termination became the default). This most commonly happens from a forgotten .catch() on a Promise-returning call, an await outside a try/catch where the awaited operation can fail, or a Promise created and never awaited or handled at all (a "floating" promise).
Reproducing the Error
A missing .catch() on a promise chain:
function processOrder(orderId: string) {
fetchOrder(orderId).then((order) => {
chargePayment(order); // if this rejects, nothing catches it
});
// no .catch() anywhere in this chain
}
An await without surrounding error handling in an async function called without its own handling:
async function syncInventory() {
const items = await fetchInventoryItems(); // if this rejects, propagates up
await updateDatabase(items);
}
syncInventory(); // called without .catch() or await — rejection is unhandled
Core Concepts Behind This Error
Node.js changed its default behavior for unhandled rejections specifically because silently ignoring a rejected Promise is dangerous — an operation failed, and your application proceeded as if nothing happened, potentially leaving data in an inconsistent state; crashing the process (rather than silently continuing) surfaces this class of bug loudly and immediately rather than letting it cause subtler downstream problems.
A "floating" promise — one created but never awaited, chained, or otherwise handled — is a distinct but related issue even when it doesn't reject, since its resolution/rejection is disconnected from your code's control flow entirely; linting rules like no-floating-promises catch this category proactively, before rejection even becomes a concern.
Every async function called without awaiting it (in a context where you're not deliberately fire-and-forgetting) is a potential source of this error, since any rejection inside that function has no attached handler in the calling code — this is the most common real-world pattern behind the warning in application code.
A global unhandled rejection handler can log or report the error but doesn't substitute for actually fixing the missing handling at the source — it's a safety net for visibility and graceful degradation, not a legitimate fix for a specific known-unhandled rejection path.
Fixing UnhandledPromiseRejectionWarning
Fix 1: Add explicit .catch() or try/catch at the point where a Promise is created or awaited, addressing the specific missing handler:
async function processOrder(orderId: string) {
try {
const order = await fetchOrder(orderId);
await chargePayment(order);
} catch (err) {
logger.error("Failed to process order", { orderId, err });
throw err; // re-throw if the caller needs to know, or handle fully here
}
}
Fix 2: Ensure async functions called at the top level (not awaited by anything) have their own handling, since there's no enclosing await to propagate the rejection to:
syncInventory().catch((err) => {
logger.error("Inventory sync failed", err);
});
Fix 3: Use eslint-plugin-promise or the TypeScript no-floating-promises rule to catch missing handlers statically, before they ever reach runtime:
// .eslintrc
{ "rules": { "@typescript-eslint/no-floating-promises": "error" } }
Fix 4: Add a global handler as a safety net for genuinely unexpected cases, logging with full context rather than letting the process crash silently or uninformatively:
process.on("unhandledRejection", (reason, promise) => {
logger.error("Unhandled rejection", { reason, promise });
// Depending on your application, consider a graceful shutdown here
// rather than letting the process continue in a potentially bad state
});
Should You Rely on a Global Handler Instead of Fixing Each Missing Catch?
No — a global handler is a safety net for visibility and graceful shutdown, not a substitute for proper error handling at each async operation that can meaningfully fail. Relying on it exclusively means every rejection gets the same generic treatment regardless of context, losing the ability to handle different failure types appropriately (retry one, fail loudly on another, degrade gracefully on a third).
Preventing Unhandled Rejections in Production
Enable no-floating-promises (or equivalent) linting to catch missing handlers statically during development, well before they can crash a running process. Add explicit error handling at every async operation that can meaningfully fail, treating each one deliberately rather than relying solely on a global catch-all handler, and keep the global handler as a final safety net that logs with full context and triggers graceful shutdown rather than an abrupt, uninformative crash.
If you're seeing this warning or a process crash from it, trace back to the specific async call missing a handler — the fix is almost always adding .catch() or wrapping the await in try/catch at that exact point, not a broader architectural change.