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.
| 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
- A missing
await.sendEmail(user)instead ofawait sendEmail(user). The function returns before the promise settles, so its rejection has nowhere to go. - Async callbacks in
forEach, timers and event listeners.items.forEach(async (item) => …),setTimeout(async () => …)andemitter.on('x', async () => …)all ignore the promise the callback returns. .then()with no.catch(). A promise chain that ends without a rejection handler.Promise.allwithout a handler. One rejected promise rejects the wholePromise.all.- Async route handlers in Express 4. Express 4 does not handle a rejected promise from an
asynchandler. See Cannot set headers after they are sent. fetchin the browser with no.catch(). A network failure, a CORS block orres.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_KEYIf 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
- Node.js: --unhandled-rejections=mode (modes and the v15 default), checked 2 Nov 2026.
- Node.js source: lib/internal/process/promises.js (
UnhandledPromiseRejectionmessage andERR_UNHANDLED_REJECTION), checked 2 Nov 2026. Output reproduced on Node.js 25.6.1. - Node.js: Event 'unhandledRejection', checked 2 Nov 2026.
- MDN: Window unhandledrejection event (cross-origin note), checked 2 Nov 2026.
- typescript-eslint: no-floating-promises and typed linting, checked 2 Nov 2026.
Related errors
- Express: Cannot set headers after they are sent:
Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client - TypeError: Failed to fetch:
TypeError: Failed to fetch - Unexpected token < is not valid JSON:
SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON - Vercel FUNCTION_INVOCATION_FAILED:
500: INTERNAL_SERVER_ERROR Code: FUNCTION_INVOCATION_FAILED - Vercel FUNCTION_INVOCATION_TIMEOUT:
504: GATEWAY_TIMEOUT Code: FUNCTION_INVOCATION_TIMEOUT
All guides: Fix an error.
Start free. Free plan · No credit card · Set up in minutes. Or try the live demo first.