Home

Fix this error · Servers and hosting

Fix: UnhandledPromiseRejection: This error originated either by throwing inside of an async function without a catch block

Why Node.js exits on an unhandled promise rejection, what each --unhandled-rejections mode does, and how to find and fix the missing await or catch.

A promise was rejected and no code handled the rejection. Since Node.js 15, Node treats that like an uncaught exception: it prints the error and stops the process with exit code 1. The fix that works most often is to find the promise that nobody awaits, await it inside a try/catch or add a .catch(), and reject with Error objects so the stack points at the cause.

FactDetail
Error (rejected with a non-Error value)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 promise rejected with the reason "…".
Error codeERR_UNHANDLED_REJECTION
Rejected with an ErrorNode prints that error's own message and stack, then exits
In the browserUncaught (in promise) followed by the reason, in the console
Usual causeAn async call without await, or a .then() with no .catch()
Usual fixawait it in a try/catch, or attach .catch()
BugTape catches itYes, automatically, in the browser and in Node.js with the default settings

What the error means

Every rejected promise needs a handler: an await inside a try/catch, a .catch(), or a second argument to .then(). When a promise rejects and no handler is attached by the time Node checks, Node emits the unhandledRejection event. What happens next depends on the --unhandled-rejections mode:

ModeWhat Node does
throw (default since Node 15)Emits unhandledRejection. If no listener is set, raises the rejection as an uncaught exception, which exits the process
strictRaises it as an uncaught exception. If that is handled, emits unhandledRejection
warnAlways prints a warning and keeps running
warn-with-error-codeEmits unhandledRejection. If no listener is set, prints a warning and sets the exit code to 1
nonePrints nothing and keeps running

The long message appears only when the rejection reason is not an Error, such as Promise.reject('boom'). Node then wraps it in an UnhandledPromiseRejection error with the code ERR_UNHANDLED_REJECTION. When the reason is an Error, Node prints that error, for example Error: db down with its stack.

In warn mode, Node prints UnhandledPromiseRejectionWarning: Unhandled promise rejection. 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(). with a rejection id, and the process keeps running.

In the browser, the same mistake prints Uncaught (in promise) and the reason in the console, and fires the unhandledrejection event on window. The page keeps running.

Common causes

  1. A missing await. sendEmail(user) instead of await sendEmail(user). The function returns before the promise settles, so its rejection has nowhere to go.
  2. Async callbacks in forEach, timers and event listeners. items.forEach(async (item) => …), setTimeout(async () => …) and emitter.on('x', async () => …) all ignore the promise the callback returns.
  3. .then() with no .catch(). A promise chain that ends without a rejection handler.
  4. Promise.all without a handler. One rejected promise rejects the whole Promise.all.
  5. Async route handlers in Express 4. Express 4 does not handle a rejected promise from an async handler. See Cannot set headers after they are sent.
  6. fetch in the browser with no .catch(). A network failure, a CORS block or res.json() on a non-JSON body rejects. See Failed to fetch and Unexpected token.

How to fix it

Find the promise from the stack

Reject with Error objects. A rejection with a string or a plain object has no stack, so Node prints only the long generic message. Then read the first frame in your own code.

// Before: no stack, generic message
return Promise.reject('user not found');

// After: message and stack point at this line
throw new Error(`user not found: ${id}`);

Await it inside try/catch

async function signup(req, res) {
  const user = await createUser(req.body);
  try {
    await sendWelcomeEmail(user); // without await, a failure here crashes the process
  } catch (err) {
    console.error('welcome email failed', err);
  }
  res.status(201).json(user);
}

Run work in the background on purpose

When you want the work to continue without waiting, attach a handler where you start it:

sendWelcomeEmail(user).catch((err) => {
  console.error('welcome email failed', err);
});

Replace forEach with for...of or Promise.all

// Before: rejections are not handled, and the function does not wait
items.forEach(async (item) => {
  await save(item);
});

// After: one at a time
for (const item of items) {
  await save(item);
}

// Or in parallel, and keep going when some fail
const results = await Promise.allSettled(items.map((item) => save(item)));
const failed = results.filter((r) => r.status === 'rejected');

Catch errors in timers and event listeners

emitter.on('order', (order) => {
  processOrder(order).catch((err) => console.error('order failed', err));
});

Let the linter find them

In TypeScript projects, the @typescript-eslint/no-floating-promises rule reports promises that are neither awaited nor handled. It is part of the type-checked configs of typescript-eslint:

// eslint.config.mjs
import js from '@eslint/js';
import { defineConfig } from 'eslint/config';
import tseslint from 'typescript-eslint';

export default defineConfig({
  files: ['**/*.{js,ts}'],
  extends: [js.configs.recommended, tseslint.configs.recommendedTypeChecked],
  languageOptions: { parserOptions: { projectService: true } },
  rules: { '@typescript-eslint/no-floating-promises': 'error' },
});

Keep a process-level listener as a last resort

A listener stops the crash, so log the error and exit, because the process may be in an unknown state.

process.on('unhandledRejection', (reason) => {
  console.error('unhandled rejection', reason);
  process.exit(1);
});

Do not switch to --unhandled-rejections=warn or none to make the crash go away. The bug is still there and later failures become harder to trace.

How to catch it in production with BugTape

Node.js. bugtape.init() reports unhandled rejections with the default throw mode, because Node raises them as uncaught exceptions. When BugTape's listener is the only one, it reports the error, waits up to 2 seconds for delivery, then lets Node stop the process as it would have without BugTape.

import * as bugtape from '@bugtape/node';

bugtape.init({ service: 'api', release: process.env.GIT_SHA }); // key from BUGTAPE_KEY

If you run Node with --unhandled-rejections=warn or none, or your app adds its own unhandledRejection listener, the rejection does not reach BugTape on its own. Add one line in your listener:

process.on('unhandledRejection', (reason) => {
  bugtape.captureException(reason);
});

A rejection with a plain value has no stack, so every such report shares one location. Rejecting with Error objects keeps different bugs in different issues.

Browser. The browser SDK listens for unhandledrejection on window and reports it automatically as Unhandled promise rejection: … with the page, the stack and the clicks, network requests and console output before it. Nothing is recorded before init() runs. Browsers do not fire the event for rejections from cross-origin scripts. If you catch a rejection yourself and still want it reported, call reportError(err), which fires the window error event the SDK listens for.

Repeats of the same rejection group into one issue with a count. Set release so a fix that does not hold reopens the issue as a regression. Your coding agent can read the issue over MCP (npx -y bugtape connect claude, or cursor or codex) and propose the missing await or .catch(). You review the pull request. Server reports have no replay or console output.

FAQ

Why did my Node app start crashing on unhandled rejections?

Node 15 changed the default --unhandled-rejections mode from a warning to throw. A rejection that used to print UnhandledPromiseRejectionWarning now stops the process. The rejection was a bug before as well. Node now makes it visible.

What does "The promise rejected with the reason" mean?

It is the value the promise was rejected with. You see this form only when the value is not an Error, such as a string or a plain object. Change the code to reject with new Error(...) and Node prints a real stack instead.

Should I add process.on('unhandledRejection') to stop the crash?

Use it to log or report the error, then exit. A listener stops Node's default crash, so a process that keeps running after an unknown failure can serve wrong results. Fix the promise that has no handler.

Is "Uncaught (in promise)" in the browser the same thing?

Yes. It is the browser's console message for a rejected promise with no handler, and it fires the unhandledrejection event on window. The page keeps running, but whatever that promise was doing failed.

Sources

All guides: Fix an error.

Start free. Free plan · No credit card · Set up in minutes. Or try the live demo first.