---
# === IDENTITY ===
id: software/debugging/nodejs-unhandled-promises/2026
canonical_question: "How do I debug unhandled promise rejections in Node.js?"
aliases:
  - "Node.js unhandled promise rejection"
  - "UnhandledPromiseRejectionWarning"
  - "Node.js PromiseRejectionHandledWarning"
  - "unhandledRejection event"
  - "Node.js async error not caught"
  - "Node.js promise not catching error"
  - "DEP0018 unhandled promise"
  - "Node.js process crash unhandled rejection"
entity_type: software_reference
domain: software > debugging > nodejs_unhandled_promises
region: global
jurisdiction: global
temporal_scope: 2020-2026

# === VERIFICATION ===
last_verified: 2026-05-17
confidence: 0.93
version: 2.1
first_published: 2026-02-17

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: stable
  last_breaking_change: "Node.js 15 (2020-10) — unhandled rejections crash by default"
  next_review: 2026-11-13
  change_sensitivity: low

# === CONSTRAINTS ===
constraints:
  - "Node.js 15+ crashes by default on unhandled rejections — code that 'worked' on Node.js 14 will crash on upgrade"
  - "process.on('unhandledRejection') is a diagnostic safety net, not a permanent fix — swallowing errors hides bugs and leaves process in inconsistent state"
  - "Never use async callbacks with Array.forEach — rejections silently escape because forEach does not await"
  - "Express 4 does not catch async route handler rejections — use express-async-errors, a manual wrapper, or upgrade to Express 5"
  - "In production, always exit after an unhandled rejection — continuing risks corrupted state, leaked connections, and silent data loss"
  - "Jest/Mocha swallow unhandled rejections as warnings, not test failures — use expect(...).rejects.toThrow() explicitly"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "Error is a synchronous thrown exception, not a rejected Promise"
    use_instead: "Standard try/catch debugging — unhandled rejections only apply to Promises"
  - condition: "Error is ECONNREFUSED or ENOTFOUND (network connectivity issue, not a missing handler)"
    use_instead: "software/debugging/nodejs-econnrefused/2026"
  - condition: "Error is a memory leak or heap out-of-memory crash, not a promise rejection"
    use_instead: "software/debugging/nodejs-memory-leaks/2026"

# === AGENT HINTS ===
inputs_needed:
  - key: node_version
    question: "Which Node.js version are you running?"
    type: choice
    options: ["Node.js 22+ (Current)", "Node.js 20 LTS", "Node.js 18 LTS", "Node.js 14 or earlier"]
  - key: framework
    question: "Which web framework are you using?"
    type: choice
    options: ["Express 4", "Express 5", "Fastify", "NestJS", "Koa", "No framework / CLI script"]
  - key: typescript
    question: "Is the project using TypeScript?"
    type: choice
    options: ["Yes", "No"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/debugging/nodejs-unhandled-promises/2026"
suggested_citation: "Source: knowledgelib.io — AI Knowledge Library (verified 2026-05-17)"

# === RELATED UNITS ===
related_kos:
  related_to:
    - id: "software/debugging/nodejs-memory-leaks/2026"
      label: "Node.js Memory Leak Debugging"
    - id: "software/debugging/nodejs-econnrefused/2026"
      label: "Node.js ECONNREFUSED Debugging"
  solves:
    - id: "software/debugging/nextjs-build-failures/2026"
      label: "Next.js Build Failures"
  often_confused_with:
    - id: "software/debugging/react-hydration-mismatch/2026"
      label: "React/Next.js Hydration Mismatch"

# === SOURCES (5-8 authoritative sources) ===
sources:
  - id: src1
    title: "Node.js — Process Event: unhandledRejection"
    author: Node.js Foundation
    url: https://nodejs.org/api/process.html#event-unhandledrejection
    type: official_docs
    published: 2025-10-01
    reliability: authoritative
  - id: src2
    title: "Node.js — Errors: Error Propagation and Interception"
    author: Node.js Foundation
    url: https://nodejs.org/api/errors.html
    type: official_docs
    published: 2025-10-01
    reliability: authoritative
  - id: src3
    title: "Better Stack — A Complete Guide to Error Handling in Node.js"
    author: Better Stack
    url: https://betterstack.com/community/guides/scaling-nodejs/nodejs-errors/
    type: technical_blog
    published: 2025-03-01
    reliability: high
  - id: src4
    title: "ESLint — no-floating-promises Rule (typescript-eslint)"
    author: TypeScript-ESLint Team
    url: https://typescript-eslint.io/rules/no-floating-promises/
    type: official_docs
    published: 2025-01-01
    reliability: high
  - id: src5
    title: "Toptal — Asynchronous Error Handling in Node.js"
    author: Toptal Engineering Blog
    url: https://www.toptal.com/nodejs/top-10-common-nodejs-developer-mistakes
    type: technical_blog
    published: 2024-06-01
    reliability: high
  - id: src6
    title: "MDN — Promise.allSettled()"
    author: Mozilla Developer Network
    url: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled
    type: official_docs
    published: 2025-01-01
    reliability: authoritative
  - id: src7
    title: "Express.js — Error Handling Middleware"
    author: Express.js Foundation
    url: https://expressjs.com/en/guide/error-handling.html
    type: official_docs
    published: 2025-01-01
    reliability: authoritative
  - id: src8
    title: "Express 5.0 Released — Focuses on Stability and Security"
    author: InfoQ
    url: https://www.infoq.com/news/2025/01/express-5-released/
    type: technical_blog
    published: 2025-01-11
    reliability: high
---

# How to Debug Unhandled Promise Rejections in Node.js

## TL;DR

- **Bottom line**: An unhandled promise rejection occurs when a Promise is rejected but no `.catch()` or `try/catch` handles the error. Since Node.js 15+, unhandled rejections crash the process by default (`--unhandled-rejections=throw`). The top causes: (1) missing `.catch()` on promise chains, (2) `async` functions without `try/catch`, (3) forgotten `await` (floating promises), (4) errors in `.then()` callbacks, (5) `Promise.all()` without error handling.
- **Key tool/command**: `node --unhandled-rejections=strict app.js` to crash immediately on unhandled rejections during development. Use ESLint rule `@typescript-eslint/no-floating-promises` to catch them at lint time.
- **Watch out for**: `async` functions in event handlers, `setTimeout` callbacks, and `Array.forEach` — `await` doesn't work in `forEach`, so rejections silently escape.
- **Works with**: Node.js 15+ (crashes by default), Node.js 12-14 (warning only). Applies to all frameworks: Express 4/5, Fastify, NestJS, Koa.

## Constraints
<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

- Node.js 15+ crashes by default on unhandled rejections — code that "worked" on Node.js 14 will crash after upgrade. [src1]
- `process.on('unhandledRejection')` is a diagnostic safety net, not a permanent fix — swallowing errors hides bugs and leaves the process in an inconsistent state. [src1, src3]
- Never use `async` callbacks with `Array.forEach` — rejections silently escape because `forEach` does not `await`. Use `for...of` or `Promise.all(arr.map(...))`. [src5]
- Express 4 does not catch async route handler rejections — use `express-async-errors`, a manual wrapper, or upgrade to Express 5 (which handles async natively since v5.0.0, released 2025-01-11). [src7, src8]
- In production, always exit after an unhandled rejection — continuing risks corrupted state, leaked connections, and silent data loss. [src1, src3]
- Jest/Mocha test runners swallow unhandled rejections as warnings, not test failures — use `expect(...).rejects.toThrow()` explicitly. [src3]

## Quick Reference

| # | Cause | Likelihood | Signature | Fix |
|---|---|---|---|---|
| 1 | Missing `.catch()` on promise chain | ~25% of cases | `UnhandledPromiseRejectionWarning` with stack trace of `.then()` | Add `.catch()` to every promise chain [src1, src2] |
| 2 | `async` function without `try/catch` | ~22% of cases | Crash at `await` line; no handler above | Wrap `await` calls in `try/catch` blocks [src2, src3] |
| 3 | Floating promise (missing `await`) | ~18% of cases | Promise fires but rejection has no handler | Add `await` or `.catch()`; enable `no-floating-promises` lint rule [src4] |
| 4 | Error thrown inside `.then()` callback | ~10% of cases | Stack trace points to `.then()` handler code | Chain `.catch()` after the `.then()` [src2] |
| 5 | `Promise.all()` without error handling | ~8% of cases | One rejection crashes; other results lost | Use `Promise.allSettled()` or wrap with `try/catch` [src6] |
| 6 | Express async route without error wrapper | ~7% of cases | Express hangs, no error response sent | Use `express-async-errors` (Express 4) or upgrade to Express 5 [src7, src8] |
| 7 | Event handler with async callback | ~4% of cases | `emitter.on('event', async fn)` — rejection escapes | Wrap async callback body in `try/catch` [src3, src5] |
| 8 | `async` in `forEach`/`map` without await | ~3% of cases | Iterations start but rejections aren't caught | Use `for...of` loop with `await` or `Promise.all(arr.map(...))` [src5] |
| 9 | Conditional `.catch()` (missing in some code paths) | ~2% of cases | Only fails in certain conditions | Ensure `.catch()` on all branches [src3] |
| 10 | Rejected promise in module top-level | ~1% of cases | Crash during `import`/`require` initialization | Wrap top-level async in IIFE with error handling [src2] |

## Decision Tree

```
START
├── Is the error "UnhandledPromiseRejectionWarning" or immediate crash?
│   ├── WARNING (Node 14 or earlier) → Add --unhandled-rejections=strict to catch early [src1]
│   └── CRASH (Node 15+) → Default behavior, read the stack trace ↓
├── Does the stack trace show a specific file and line?
│   ├── YES → Go to that line
│   │   ├── Is there an `await` without `try/catch`? → Add try/catch [src2, src3]
│   │   ├── Is there a `.then()` without `.catch()`? → Add .catch() [src2]
│   │   ├── Is there a function call returning a Promise without await? → Floating promise — add await [src4]
│   │   └── Is it inside a callback (event handler, setTimeout, forEach)? → Wrap in try/catch [src5]
│   └── NO (stack trace is unhelpful) ↓
├── Enable async stack traces: node --async-stack-traces app.js
│   └── Still unclear → Add process.on('unhandledRejection') with full logging [src1]
├── Is it in an Express route?
│   ├── YES, Express 4 → Use express-async-errors or wrap route handler [src7]
│   ├── YES, Express 5 → Async errors are caught automatically — check error middleware [src8]
│   └── NO ↓
├── Is it in a loop processing multiple items?
│   ├── YES → Using forEach? → Switch to for...of with await [src5]
│   │   └── Using Promise.all? → Switch to Promise.allSettled or add try/catch [src6]
│   └── NO ↓
└── Add global handler as safety net: process.on('unhandledRejection') [src1]
```

## Step-by-Step Guide

### 1. Understand the Node.js version behavior

The way Node.js handles unhandled rejections changed significantly across versions. [src1]

```
Node.js 10-14: Warning only (DEP0018 deprecation warning), process continues
Node.js 15+:  Throws, process crashes (--unhandled-rejections=throw is default)
```

```bash
# Check your Node.js version
node -v

# Force strict mode in development (crash immediately)
node --unhandled-rejections=strict app.js

# Modes: throw (default 15+), warn (default <15), strict, none
node --unhandled-rejections=throw app.js
```

**Verify**: Run your app with `--unhandled-rejections=strict` — any unhandled rejections will crash immediately with a stack trace.

### 2. Add a global handler for diagnosis

While you find and fix the root cause, add a global handler to log full details. [src1]

```javascript
// Add at the top of your entry file (app.js / index.js)
process.on('unhandledRejection', (reason, promise) => {
  console.error('UNHANDLED REJECTION at:', promise);
  console.error('Reason:', reason);
  console.error('Stack:', reason?.stack || 'No stack trace');

  // In production: log to error tracker, then exit
  // process.exit(1);
});

// Also catch uncaught exceptions (synchronous)
process.on('uncaughtException', (error) => {
  console.error('UNCAUGHT EXCEPTION:', error);
  process.exit(1);
});
```

**Verify**: Run your app and trigger the failing code path — you should see the full error details logged.

### 3. Fix missing try/catch in async functions

The most common pattern: `async` function without error handling. [src2, src3]

```javascript
// ❌ WRONG — rejection escapes
async function fetchUserData(userId) {
  const response = await fetch(`/api/users/${userId}`);  // Can reject!
  const data = await response.json();
  return data;
}

// ✅ CORRECT — error caught and handled
async function fetchUserData(userId) {
  try {
    const response = await fetch(`/api/users/${userId}`);
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    const data = await response.json();
    return data;
  } catch (error) {
    console.error(`Failed to fetch user ${userId}:`, error.message);
    throw error;  // Re-throw if caller needs to handle it
  }
}
```

**Verify**: The error is caught and logged, not crashing the process.

### 4. Find and fix floating promises

A "floating promise" is a promise that's not awaited or `.catch()`-ed. [src4]

```javascript
// ❌ WRONG — floating promise (no await, no .catch)
function handleRequest(req, res) {
  saveToDatabase(req.body);  // Returns a promise, but nobody handles it!
  res.json({ ok: true });
}

// ✅ CORRECT — await the promise
async function handleRequest(req, res) {
  try {
    await saveToDatabase(req.body);
    res.json({ ok: true });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
}

// ✅ ALSO CORRECT — fire-and-forget with explicit .catch()
function handleRequest(req, res) {
  saveToDatabase(req.body).catch(err => {
    console.error('Background save failed:', err);
  });
  res.json({ ok: true });
}
```

**Verify**: Enable the ESLint rule to catch these at build time:
```json
// .eslintrc.json (requires @typescript-eslint/eslint-plugin)
{
  "rules": {
    "@typescript-eslint/no-floating-promises": "error"
  }
}
```

### 5. Fix Promise.all() error handling

`Promise.all()` rejects as soon as one promise rejects, losing other results. [src6]

```javascript
// ❌ RISKY — one failure rejects everything
const results = await Promise.all([
  fetchUser(1),
  fetchUser(2),   // If this fails, you lose result from fetchUser(1)
  fetchUser(3),
]);

// ✅ CORRECT — Promise.allSettled handles partial failures
const results = await Promise.allSettled([
  fetchUser(1),
  fetchUser(2),
  fetchUser(3),
]);

const successes = results
  .filter(r => r.status === 'fulfilled')
  .map(r => r.value);
const failures = results
  .filter(r => r.status === 'rejected')
  .map(r => r.reason);

if (failures.length > 0) {
  console.error(`${failures.length} fetches failed:`, failures);
}
```

**Verify**: All operations complete; failures are logged, successes are used.

### 6. Fix Express async routes

Express 4 doesn't natively catch async route handler rejections. Express 5 (released 2025-01-11) fixes this. [src7, src8]

```javascript
// ❌ WRONG — Express 4 doesn't catch async rejections
app.get('/users/:id', async (req, res) => {
  const user = await db.getUser(req.params.id);  // If this throws → unhandled
  res.json(user);
});

// ✅ FIX 1 — Use express-async-errors (Express 4, simplest)
require('express-async-errors');
app.get('/users/:id', async (req, res) => {
  const user = await db.getUser(req.params.id);  // Now auto-forwarded to error middleware
  res.json(user);
});

// ✅ FIX 2 — Manual wrapper (Express 4)
const asyncHandler = (fn) => (req, res, next) =>
  Promise.resolve(fn(req, res, next)).catch(next);

app.get('/users/:id', asyncHandler(async (req, res) => {
  const user = await db.getUser(req.params.id);
  res.json(user);
}));

// ✅ FIX 3 — Express 5 (handles async natively, released 2025-01-11)
// Express 5 automatically catches rejected promises in route handlers
// No wrapper needed — just ensure error middleware has 4 params
app.get('/users/:id', async (req, res) => {
  const user = await db.getUser(req.params.id);
  res.json(user);
});

// Error middleware (required for all approaches — must have 4 params)
app.use((err, req, res, next) => {
  console.error('Error:', err.message);
  res.status(err.status || 500).json({ error: err.message });
});
```

**Verify**: Trigger an error in the route — Express responds with the error middleware, no crash.

### 7. Fix async in loops

`Array.forEach` doesn't await async callbacks — rejections escape silently. [src5]

```javascript
// ❌ WRONG — forEach doesn't await, rejections are unhandled
items.forEach(async (item) => {
  await processItem(item);  // Rejection has no handler!
});

// ✅ CORRECT — for...of with await (sequential)
for (const item of items) {
  await processItem(item);
}

// ✅ CORRECT — Promise.all with map (parallel)
await Promise.all(items.map(async (item) => {
  await processItem(item);
}));

// ✅ CORRECT — Promise.allSettled for partial failure tolerance
const results = await Promise.allSettled(
  items.map(item => processItem(item))
);
```

**Verify**: All items are processed; errors are caught, not silently dropped.

## Code Examples

### Global error handler with graceful shutdown

> Full script: [global-error-handler-with-graceful-shutdown.js](scripts/global-error-handler-with-graceful-shutdown.js) (43 lines)

```javascript
// Input:  App crashes randomly from various unhandled rejections
// Output: Comprehensive global handler with logging and graceful shutdown
const process = require('process');
let isShuttingDown = false;
process.on('unhandledRejection', (reason, promise) => {
# ... (see full script)
```

### Safe async utility functions

> Full script: [safe-async-utility-functions.js](scripts/safe-async-utility-functions.js) (42 lines)

```javascript
// Input:  Need reusable patterns for safe async operations
// Output: Utility functions that prevent unhandled rejections
/**
 * Wrap an async function to prevent unhandled rejections.
 * Returns [error, result] tuple (Go-style error handling).
# ... (see full script)
```

### Express app with comprehensive error handling

> Full script: [express-app-with-comprehensive-error-handling.js](scripts/express-app-with-comprehensive-error-handling.js) (38 lines)

```javascript
// Input:  Express app with async routes that crash on errors
// Output: Production-ready error handling setup
const express = require('express');
require('express-async-errors');  // Auto-catch async rejections
const app = express();
# ... (see full script)
```

## Anti-Patterns

### Wrong: Async function without try/catch

```javascript
// ❌ BAD — rejection propagates as unhandled [src2, src3]
app.get('/data', async (req, res) => {
  const data = await fetchExternalAPI();  // If API is down → crash
  res.json(data);
});
```

### Correct: Wrap in try/catch

```javascript
// ✅ GOOD — error caught and handled [src2, src3]
app.get('/data', async (req, res, next) => {
  try {
    const data = await fetchExternalAPI();
    res.json(data);
  } catch (error) {
    next(error);  // Forward to Express error middleware
  }
});
```

### Wrong: forEach with async callbacks

```javascript
// ❌ BAD — forEach doesn't handle async rejections [src5]
async function processAll(items) {
  items.forEach(async (item) => {
    await sendEmail(item.email);  // If one fails, rejection is unhandled
  });
  console.log('Done');  // Logs immediately, before emails send!
}
```

### Correct: for...of or Promise.all

```javascript
// ✅ GOOD — proper sequential processing [src5]
async function processAll(items) {
  for (const item of items) {
    try {
      await sendEmail(item.email);
    } catch (err) {
      console.error(`Failed to email ${item.email}:`, err.message);
    }
  }
  console.log('Done');  // Logs after all emails sent
}

// ✅ ALSO GOOD — parallel with error handling
async function processAllParallel(items) {
  const results = await Promise.allSettled(
    items.map(item => sendEmail(item.email))
  );
  const failed = results.filter(r => r.status === 'rejected');
  if (failed.length) console.error(`${failed.length} emails failed`);
}
```

### Wrong: Fire-and-forget without .catch()

```javascript
// ❌ BAD — floating promise, rejection unhandled [src4]
function logUserAction(userId, action) {
  db.insertLog({ userId, action, timestamp: new Date() });
  // Returns promise but nobody handles it!
}
```

### Correct: Always handle the promise

```javascript
// ✅ GOOD — explicit .catch() for fire-and-forget [src4]
function logUserAction(userId, action) {
  db.insertLog({ userId, action, timestamp: new Date() })
    .catch(err => console.error('Log insert failed:', err.message));
}

// ✅ ALSO GOOD — use void to signal intentional fire-and-forget
async function logUserAction(userId, action) {
  void db.insertLog({ userId, action, timestamp: new Date() })
    .catch(err => console.error('Log insert failed:', err.message));
}
```

## Common Pitfalls

- **`async` in `forEach` doesn't work**: `Array.forEach` doesn't handle async callbacks — rejections become unhandled. Use `for...of` with `await` or `Promise.all()` with `.map()`. [src5]
- **Express 4 doesn't catch async errors**: Async route handlers that throw or reject bypass Express error middleware entirely. Use `express-async-errors` or a manual wrapper. Express 5 (released 2025-01-11) fixes this natively — it automatically forwards rejected promises to error middleware. [src7, src8]
- **`process.on('unhandledRejection')` is a safety net, not a fix**: Using the global handler to silently swallow errors hides bugs. Log, alert, and fix the root cause. [src1, src3]
- **Conditional promise chains**: If a `.catch()` is only on one branch of an `if/else`, the other branch's rejection is unhandled. Always catch on all paths. [src3]
- **`setTimeout` / `setInterval` with async callbacks**: Timer callbacks run outside the promise chain. Rejections inside `setTimeout(async () => {...})` are unhandled unless wrapped in `try/catch`. [src5]
- **Node.js 15+ crash vs 14 warning**: Code that "worked" on Node.js 14 (with warnings) will crash on 15+. Upgrading Node.js versions may expose hidden bugs. [src1]
- **`Error.isError()` for robust checking (Node.js 24+)**: Use `Error.isError(reason)` instead of `reason instanceof Error` in the global handler — it works across realms (iframes, vm contexts). [src2]

## Diagnostic Commands

```bash
# Run with strict mode — crash immediately on unhandled rejection
node --unhandled-rejections=strict app.js

# Run with async stack traces (better debugging)
node --async-stack-traces app.js

# Both flags together
node --unhandled-rejections=strict --async-stack-traces app.js

# Check which Node.js mode is default for your version
node -e "console.log(process.version, process.execArgv)"

# Find floating promises in TypeScript projects
npx eslint . --rule '{"@typescript-eslint/no-floating-promises": "error"}'

# Find all .catch() patterns in codebase
grep -rn "\.catch(" --include="*.js" --include="*.ts" src/

# Find all async functions without try/catch
grep -rn "async.*=>" --include="*.js" --include="*.ts" src/ | head -30

# Find forEach with async
grep -rn "forEach(async" --include="*.js" --include="*.ts" src/
grep -rn "\.forEach(async" --include="*.js" --include="*.ts" src/

# Test global handler
node -e "
process.on('unhandledRejection', r => console.log('CAUGHT:', r));
Promise.reject(new Error('test'));
"

# Check Express version (5+ handles async natively)
node -e "console.log(require('express/package.json').version)"
```

## Version History & Compatibility

| Version | Behavior | Key Changes |
|---|---|---|
| Node.js 26 (Current) | Crash (default) | Same default `--unhandled-rejections=throw` behavior; no DEP0018 changes; documented at v26.1.0 [src1, src2] |
| Node.js 25 | Crash (default) | Removes `multipleResolves` event; portable compile cache; Web Storage default [src1] |
| Node.js 24 | Crash (default) | V8 13.6; `Error.isError()` for cross-realm checks; `using`/`await using` for resource cleanup; AsyncLocalStorage perf improvements [src1, src2] |
| Node.js 23 | Crash (default) | 15-20% faster bootstrap; `require(esm)` unflagged [src1] |
| Node.js 22 LTS | Crash (default) | `error.cause` in promise rejections; improved diagnostics [src1, src2] |
| Node.js 20 LTS | Crash (default) | `Promise.withResolvers()` for cleaner promise patterns [src1] |
| Node.js 18 LTS (EOL) | Crash (default) | Built-in `fetch()` (experimental); async errors catchable [src1] |
| Node.js 16 (EOL) | Crash (default) | `--unhandled-rejections=throw` became default behavior [src1] |
| Node.js 15 (EOL) | Crash (default) | First version to crash by default on unhandled rejections [src1] |
| Node.js 14 (EOL) | Warning only | `UnhandledPromiseRejectionWarning`; deprecation DEP0018 [src1] |
| Node.js 12 (EOL) | Warning only | `--unhandled-rejections` flag introduced [src1] |

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Process crashes with "unhandled rejection" | Error is a caught exception (synchronous throw) | Standard `try/catch` debugging |
| `UnhandledPromiseRejectionWarning` in logs | Error is `ECONNREFUSED` (connection issue) | Check service connectivity first |
| Express route hangs without responding | Error is in a callback-based API (not Promise) | Check callback error parameter |
| Upgrading Node.js version causes new crashes | Error is logged but handled correctly | Verify handler is working as expected |
| Jest/Mocha shows warning but test passes | Error is memory-related (heap OOM) | Node.js memory leak debugging |

## Important Caveats

- **Rejections are not the same as thrown errors**: A rejected promise only becomes an "unhandled rejection" after the microtask queue processes. Attaching `.catch()` even slightly later can prevent the error — but this is fragile and not recommended.
- **`process.on('unhandledRejection')` prevents crash but hides bugs**: If you handle the event and don't exit, the process continues in a potentially inconsistent state. In production, log the error and restart.
- **Express 5 changes everything**: Express 5 (stable release 2025-01-11) natively handles async route handler rejections, forwarding them to error middleware automatically. This eliminates the need for `express-async-errors` or manual wrappers. Express 5 requires Node.js 18+. [src8]
- **`Promise.allSettled()` vs `Promise.all()`**: `Promise.all()` fails fast — the first rejection rejects the whole batch. `Promise.allSettled()` waits for all promises and reports individual outcomes. Choose based on whether partial results are useful.
- **Jest/Mocha test frameworks**: Test runners have their own unhandled rejection handling. An unhandled rejection in a test may show as a warning rather than a test failure — use `expect(...).rejects.toThrow()` explicitly.
- **Node.js 24+ `using`/`await using` (explicit resource management)**: The `await using` syntax (V8 13.6) provides deterministic cleanup for resources like database connections and file handles, reducing the chance of rejections from leaked resources. [src2]
- **Node.js 25 removed `multipleResolves` event**: The `process.on('multipleResolves')` event was removed in Node.js 25. If you relied on it for diagnostics, switch to the `unhandledRejection` event and `Error.isError()` checks. [src1]

## Related Units
<!-- Generated from related_kos frontmatter -->

- [Node.js Memory Leak Debugging](/software/debugging/nodejs-memory-leaks/2026)
- [Node.js ECONNREFUSED Debugging](/software/debugging/nodejs-econnrefused/2026)
- [Next.js Build Failures](/software/debugging/nextjs-build-failures/2026)
- [React/Next.js Hydration Mismatch](/software/debugging/react-hydration-mismatch/2026)
