← Debugging Guides

阅读中文版本

Node.js — Unhandled promise rejection

Find the promise that rejected without a handler, fix floating async calls and async event handlers, and add a process-level safety net without swallowing errors.

intermediatedebuggingnodejsasync

Symptoms

  • UnhandledPromiseRejection: This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch()
  • The process exits with code 1 even though the request appeared to succeed
  • The stack trace points to a promise chain but not to the call site that forgot to await
  • The failure appears only when a downstream dependency rejects

Possible causes

  • An async function is called without await or .catch()
  • An async event handler returns a rejected promise that the emitter ignores
  • A try/catch wraps a call that is not awaited, so the rejection escapes
  • A rejection occurs inside setTimeout or a callback without a handler
  • A process-level handler swallows rejections without logging or exiting

Diagnosis steps

Enable rejection tracing

node --trace-warnings --unhandled-rejections=strict server.js

Interpretation

Strict mode turns unhandled rejections into uncaught exceptions with a stack trace. Trace warnings adds async context where available.

Log the reason and the promise context

process.on('unhandledRejection', (reason, promise) => console.error(reason, promise))

Interpretation

Temporary instrumentation identifies the promise and reason. Remove or replace it with a deliberate handler before production.

Search for floating async calls

grep -RInE 'async |await |\.then\(|\.catch\(' src | head -50

Interpretation

Calls to async functions without await or catch are the usual source, especially in event handlers and background jobs.

Root cause

A promise rejected and no handler was attached. Node reports an unhandled rejection, and in modern versions the default behavior is to terminate the process.

Fix

Await async calls inside try/catch, attach .catch() to intentional fire-and-forget work, wrap event handlers so returned promises are handled, and add a process-level handler that logs and exits non-zero for unknown rejections. Do not swallow errors silently.

Risk note

A process-level handler that only logs can leave the service running with corrupted state. Decide per error whether to retry, degrade, or restart, and make the decision explicit.

Verify the fix

Reproduce the rejection with a test that forces the dependency to fail, confirm the error is logged with context, and verify the process exits or recovers according to policy. Add a lint rule or code review check for floating promises.

A rejected promise needs a handler

Node reports an unhandled rejection when a promise rejects and nothing handles it. Since Node 15, the default is to terminate the process. The stack often points into the promise chain, not to the call site that forgot to await. Run with --trace-warnings --unhandled-rejections=strict to get a more useful trace.

// Floating: the rejection escapes
loadUser(id).then((user) => res.json(user));

// Handled: await inside try/catch
try {
  const user = await loadUser(id);
  res.json(user);
} catch (error) {
  next(error);
}

Async event handlers

An event emitter does not await an async listener. A rejected promise returned by an async on('message') handler becomes an unhandled rejection. Wrap the body in try/catch, or use a helper that attaches a catch handler and reports the failure. The same applies to setTimeout(async () => ...).

Fire-and-forget needs a catch

Some work is intentionally detached, such as a metrics flush. Attach .catch() and decide what happens on failure: log, retry, or mark degraded. Do not use void to silence the compiler while leaving the rejection unhandled.

void flushMetrics().catch((error) => logger.error({ error }, 'metrics flush failed'));

Decide the process policy

A process-level unhandledRejection handler can log and exit, which is safer than continuing with unknown state. If the service can recover, document which errors are recoverable and why. Test the handler by forcing a rejection and asserting the exit code or recovery behavior.

Fix and verify

Fix the source by awaiting async calls inside try/catch, attaching .catch() to intentional detached work, and wrapping async event handlers so returned promises are handled. Verify with a test that forces the dependency to reject, run the service with --unhandled-rejections=strict, and confirm the error is logged with operation context and the process follows the documented exit or recovery policy. Add a lint or review check for floating promises.