# How to Catch and Handle Unhandled Promise Rejections in Node.js: A Complete Guide

> Master unhandled promise rejections in Node.js with a two-layer strategy. Learn to catch and handle errors effectively for robust applications.

- Repository: [Yoni Goldberg/nodebestpractices](https://github.com/goldbergyoni/nodebestpractices)
- Tags: how-to-guide
- Published: 2026-02-26

---

**Use a two-layer strategy: always attach local `.catch()` or `try/catch` blocks to promises, then register a global `process.on('unhandledRejection')` listener that re-throws errors to your central `uncaughtException` handler.**

Modern Node.js applications rely heavily on promises and `async/await` syntax. When a promise rejects without a corresponding `.catch()` handler or `try/catch` block, the error bubbles up to the event loop and emits an **`unhandledRejection`** event. According to the **goldbergyoni/nodebestpractices** repository, relying solely on this warning is dangerous because your process may continue running in an undefined error state. Implementing a robust strategy to catch and handle unhandled promise rejections in Node.js ensures predictable behavior and prevents silent failures.

## Why Unhandled Promise Rejections Are Dangerous

When a promise rejection goes unhandled, Node.js prints a deprecation warning to stderr, but the process keeps running by default. This creates a **silent failure mode** where your application might serve requests while internal state is corrupted. In production environments, this leads to memory leaks, inconsistent data, and difficult-to-debug crashes that occur hours after the original error.

## The Two-Layer Strategy to Handle Unhandled Promise Rejections

The **nodebestpractices** documentation recommends a defensive architecture with two complementary layers: local handling for expected errors and global fallback for unexpected rejections.

### Layer 1: Local Error Handling with .catch() and try/catch

Always attach error handlers at the point where promises originate. This prevents rejections from ever becoming "unhandled" and allows context-specific recovery.

```javascript
// Using .catch() with promise chains
DAL.getUserById(1)
  .then(user => {
    if (!user.isAlive) throw new Error('User is dead');
  })
  .catch(err => {
    // Handle the error where it originated
    errorManagement.handler.handleError(err);
  });

```

```javascript
// Using async/await with try/catch
async function processUser(id) {
  try {
    const user = await DAL.getUserById(id);
    if (!user.isAlive) throw new Error('User is dead');
  } catch (err) {
    errorManagement.handler.handleError(err);
  }
}

```

### Layer 2: Global Fallback with process.on('unhandledRejection')

Despite local handling, programming errors or third-party library bugs may still produce stray rejections. Register a global listener to catch these as a last resort.

## Implementing the Global Safety Net

The recommended pattern in [`sections/errorhandling/catchunhandledpromiserejection.md`](https://github.com/goldbergyoni/nodebestpractices/blob/main/sections/errorhandling/catchunhandledpromiserejection.md) converts unhandled rejections into exceptions that your existing `uncaughtException` handler can process. This eliminates duplicate logic and ensures consistent cleanup.

```javascript
// Global fallback for unhandled promise rejections
process.on('unhandledRejection', (reason, promise) => {
  // Forward to the central error handler by converting to exception
  throw reason;
});

// Global handler for any uncaught synchronous errors
process.on('uncaughtException', (error) => {
  errorManagement.handler.handleError(error);
  if (!errorManagement.handler.isTrustedError(error)) {
    // Exit the process after cleanup – avoid undefined state
    process.exit(1);
  }
});

```

**Why re-throw inside `unhandledRejection`?**  
Converting the rejection to a thrown exception allows your existing `uncaughtException` logic to handle diagnostics, logging, and graceful shutdown from a single code path. This prevents divergence between how you handle synchronous and asynchronous fatal errors.

## Local Handling Best Practices

When implementing local error handling, avoid these common anti-patterns demonstrated in the **nodebestpractices** examples:

**Anti-pattern: Missing .catch()**

```javascript
// This will trigger the global unhandledRejection handler
DAL.getUserById(1).then(user => {
  if (!user.isAlive) throw new Error('User is dead');
});

```

**Pattern: Proper error attachment**

Always ensure every promise chain terminates with a `.catch()` or is wrapped in `try/catch` when using `await`.

## Key Files in the nodebestpractices Repository

The following files in **goldbergyoni/nodebestpractices** provide authoritative guidance on this pattern:

- [`sections/errorhandling/catchunhandledpromiserejection.md`](https://github.com/goldbergyoni/nodebestpractices/blob/main/sections/errorhandling/catchunhandledpromiserejection.md) – Core documentation explaining the two-layer strategy, including the rationale for re-throwing rejections to the `uncaughtException` handler.
- [`README.md`](https://github.com/goldbergyoni/nodebestpractices/blob/main/README.md) – Overview linking to all error-handling best practices sections.
- `sections/errorhandling/` – Additional context for comprehensive error management strategies in Node.js applications.

## Summary

- **Always use local handling**: Attach `.catch()` to promise chains or wrap `await` calls in `try/catch` blocks to handle expected errors at their source.
- **Implement a global fallback**: Register `process.on('unhandledRejection')` to catch stray rejections from programming errors or library bugs.
- **Re-throw to unify handling**: Convert unhandled rejections to exceptions by throwing the reason inside the rejection handler, allowing your existing `uncaughtException` logic to manage cleanup and shutdown.
- **Exit on fatal errors**: When global handlers catch untrusted (unexpected) errors, gracefully shut down the process after logging to avoid running in a corrupted state.

## Frequently Asked Questions

### What happens if I don't handle unhandled promise rejections?

If you don't catch and handle unhandled promise rejections in Node.js, the process emits a deprecation warning but continues running by default. This creates a dangerous silent failure mode where your application may serve requests while internal state is corrupted, leading to memory leaks, data inconsistency, and delayed crashes that are difficult to diagnose.

### Why should I re-throw unhandled rejections instead of handling them directly?

Re-throwing the rejection reason inside `process.on('unhandledRejection')` converts the asynchronous error into a synchronous exception. This allows your existing `process.on('uncaughtException')` handler to process both types of errors through a single code path, eliminating duplicate logic and ensuring consistent logging, diagnostics, and graceful shutdown procedures.

### Is process.on('unhandledRejection') enough to catch all async errors?

No, `process.on('unhandledRejection')` only catches promise rejections that lack a `.catch()` handler. It does not catch errors thrown inside synchronous code, which are handled by `uncaughtException`, or operational errors that are properly caught locally. You need both local error handling (`.catch()` or `try/catch`) and global listeners to build a comprehensive defense.

### How do I handle unhandled rejections in Express.js applications?

In Express.js applications, unhandled rejections in route handlers often occur when an async route lacks error handling middleware. First, ensure all async routes use a catch block or an Express wrapper that forwards errors to `next(err)`. Second, implement the global `process.on('unhandledRejection')` listener that re-throws to your `uncaughtException` handler, ensuring any stray rejections trigger your centralized error management and graceful shutdown logic.