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

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.

| Fact | Detail |
|---|---|
| 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 code | `ERR_UNHANDLED_REJECTION` |
| Rejected with an Error | Node prints that error's own message and stack, then exits |
| In the browser | `Uncaught (in promise)` followed by the reason, in the console |
| Usual cause | An async call without `await`, or a `.then()` with no `.catch()` |
| Usual fix | `await` it in a `try`/`catch`, or attach `.catch()` |
| BugTape catches it | Yes, 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:

| Mode | What 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 |
| `strict` | Raises it as an uncaught exception. If that is handled, emits `unhandledRejection` |
| `warn` | Always prints a warning and keeps running |
| `warn-with-error-code` | Emits `unhandledRejection`. If no listener is set, prints a warning and sets the exit code to 1 |
| `none` | Prints 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](/guides/express-cannot-set-headers-after-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](/guides/typeerror-failed-to-fetch/) and [Unexpected token](/guides/unexpected-token-not-valid-json/).

## 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.

```js
// 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

```js
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:

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

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

```js
// 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

```js
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:

```js
// 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.

```js
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.

```js
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:

```js
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

- [Node.js: --unhandled-rejections=mode](https://nodejs.org/api/cli.html#--unhandled-rejectionsmode) (modes and the v15 default), checked 2 Nov 2026.
- [Node.js source: lib/internal/process/promises.js](https://github.com/nodejs/node/blob/main/lib/internal/process/promises.js) (`UnhandledPromiseRejection` message and `ERR_UNHANDLED_REJECTION`), checked 2 Nov 2026. Output reproduced on Node.js 25.6.1.
- [Node.js: Event 'unhandledRejection'](https://nodejs.org/api/process.html#event-unhandledrejection), checked 2 Nov 2026.
- [MDN: Window unhandledrejection event](https://developer.mozilla.org/en-US/docs/Web/API/Window/unhandledrejection_event) (cross-origin note), checked 2 Nov 2026.
- [typescript-eslint: no-floating-promises](https://typescript-eslint.io/rules/no-floating-promises/) and [typed linting](https://typescript-eslint.io/getting-started/typed-linting/), checked 2 Nov 2026.

## Related errors

- [Express: Cannot set headers after they are sent](/guides/express-cannot-set-headers-after-sent/): `Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client`
- [TypeError: Failed to fetch](/guides/typeerror-failed-to-fetch/): `TypeError: Failed to fetch`
- [Unexpected token < is not valid JSON](/guides/unexpected-token-not-valid-json/): `SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON`
- [Vercel FUNCTION_INVOCATION_FAILED](/guides/vercel-function-invocation-failed/): `500: INTERNAL_SERVER_ERROR Code: FUNCTION_INVOCATION_FAILED`
- [Vercel FUNCTION_INVOCATION_TIMEOUT](/guides/vercel-function-invocation-timeout/): `504: GATEWAY_TIMEOUT Code: FUNCTION_INVOCATION_TIMEOUT`

All guides: [Fix an error](/guides/).

[Start free](/console/register?utm_source=bugtape_seo&utm_campaign=error-guides). Free plan · No credit card · Set up in minutes. Or [try the live demo](/demo/?from=landing&start=1#capture) first.
