Handle Async Errors and Unhandled Rejections in Node.js
Catches floating promises, async EventEmitter listeners, and Express 4 async handlers whose rejections crash the process or never reach an error handler.
What it flags
import { Router } from 'express';
import { createOrder } from '../services/orders';
import { sendReceipt } from '../services/email';
export const ordersRouter = Router();
// Express 4 ignores the promise this handler returns
ordersRouter.post('/orders', async (req, res) => {
const order = await createOrder(res.locals.user.id, req.body.items);
sendReceipt(order.id);
res.status(201).json(order);
});import { Router } from 'express';
import { createOrder } from '../services/orders';
import { sendReceipt } from '../services/email';
import { logger } from '../logger';
export const ordersRouter = Router();
ordersRouter.post('/orders', async (req, res, next) => {
try {
const order = await createOrder(res.locals.user.id, req.body.items);
sendReceipt(order.id).catch((err) =>
logger.error({ err, orderId: order.id }, 'Receipt email failed'),
);
res.status(201).json(order);
} catch (err) {
next(err);
}
});Why it matters
Since Node.js 15, an unhandled promise rejection terminates the process by default, so one missing .catch() on a background email call can drop every in-flight request on that instance. In Express 4, a rejected async route handler never reaches the error middleware; Express 5 forwards these rejections, so the version in package.json decides whether the same code is safe. These bugs pass review because the happy path works, and the missing handler only matters when a dependency fails.
Agent instructions
cubic checks every pull request against these instructions. Paste them into AGENTS.md or Cursor rules and your AI coding tool follows them while it writes code.
AGENTS.md
Add this section to AGENTS.md at the repository root. Codex, Cursor, and other AI coding tools read it.
## Handle Async Errors and Unhandled Rejections in Node.js
Applies to `**/*.js`, `**/*.mjs`, `**/*.cjs`, `**/*.ts`, `**/*.mts`, `**/*.cts`.
Review Node.js async code for promise rejections and errors that escape every handler.
Flag changes that:
- Call a promise-returning function without `await`, `return`, or a `.catch()` handler, including inside `setTimeout` and `setInterval` callbacks.
- Pass an async callback to `forEach` or another method that ignores its return value, so failures go unhandled and the caller continues before the work finishes.
- Return a promise from inside a `try` block without `await`, so the `catch` block never sees its rejection.
- Register async listeners with `on()` or `once()` on EventEmitters, streams, or sockets without catching errors inside the listener.
- Add async route handlers or middleware to Express 4, or another framework that ignores returned promises, without a try/catch that calls `next(err)` or a wrapper that does.
- Swallow rejections with `.catch(() => {})` or an empty `catch` on writes or external calls whose failure the caller needs to know about.
- Add `process.on('uncaughtException')` or `process.on('unhandledRejection')` handlers that log and keep the process serving requests.
Do not flag:
- Async handlers in frameworks that forward rejections to their error handling, such as Express 5, Fastify, Koa, and NestJS.
- Deliberate fire-and-forget calls that attach a `.catch()` which logs or reports the error.
- Async listeners on emitters created with `captureRejections: true` that also have an `error` listener or a `Symbol.for('nodejs.rejection')` method to receive the rejection. Without one, the forwarded error still crashes the process.
Checks files matching **/*.js, **/*.mjs, **/*.cjs, **/*.ts, **/*.mts, **/*.cts.
More Node.js agents
See all Node.js code review standards
Run this agent on every pull request
cubic reviews every pull request against the agents you turn on and comments on the lines that break them.