Unhandled promise rejection, explained and fixed

A promise that rejects with no rejection handler attached produces this warning in browsers (Uncaught (in promise)) and, since Node 15, terminates the process in Node.js with ERR_UNHANDLED_REJECTION. The failure itself is ordinary, a network error, a thrown exception inside async code, a rejected API call. The problem is purely that nothing was listening: the code started asynchronous work and provided no path for its failure to land on.

How to find the source

  1. Read the rejection value and stack: the console or crash output shows the error the promise rejected with and, in modern environments, a stack pointing at the async code that threw. That throw site is the origin; the missing handler is wherever that promise was created or returned.
  2. Search for fire-and-forget calls: an async function invoked without await and without .catch(), an event handler calling async code bare, a forEach with an async callback (which drops the promises it creates), and promise chains where the .catch is attached to a different chain than the one that rejects.

The fixes, in order

  1. Handle failures where the promise is consumed: try { await work() } catch (e) { ... } or work().catch(...), with a real response to failure (surface it, retry deliberately, or log it with context), not an empty catch.
  2. Return and await promises inside loops correctly: use for...of with await for sequential work or collect the promises and await Promise.all(...) (with allSettled when partial failure is acceptable), never async forEach callbacks whose promises vanish.
  3. In components and handlers, attach the outcome to state: an error state the UI can show, set from the catch, so failures become visible instead of silent.
  4. Add a last-resort observer, not a cure: window.addEventListener('unhandledrejection') in browsers or process.on('unhandledRejection') in Node can log what slips through so it can be found and fixed; in Node it also changes the crash behavior, which is a reason to use it deliberately and temporarily while hunting, not as permanent policy.
Node made unhandled rejections fatal on purpose: silently dropped failures corrupt state and hide outages. Treat each occurrence as a bug report with a stack trace, and fix the missing handler at its source rather than installing a global handler that swallows them.

Common questions

Why does the same code only warn in the browser but crash my server? Different platform decisions: browsers log unhandled rejections and continue; Node's maintainers concluded a server that loses track of failed work is unsafe to keep running, so unhandled rejections throw since Node 15.

Where exactly should the catch go? On the promise chain that can reject, at the point where the result is consumed. A catch on a different variable or an earlier chain does not cover it; when in doubt, the handler belongs where the async function was called.

Can a rejection be “handled late” and still warn? Yes. If the rejection happens before any handler is attached (for example the handler is added in a later tick), the warning can fire even though a catch exists. Attach handlers when the promise is created.

Related errors

TypeError: is not a function · TypeError: Network request failed · Node: JavaScript heap out of memory