All posts
nodejspromises

Fixing UnhandledPromiseRejectionWarning in Node.js

Why Node.js warns about unhandled promise rejections, why it now crashes processes by default, and how to fix it properly.

SR

Suhail Roushan

August 6, 2026

·
5 min read
·
0 views

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.

Related posts

Written by Suhail Roushan — Full-stack developer. More posts on AI, Next.js, and building products at suhailroushan.com/blog.

Get in touch