# No unhandled promise rejections in production

- **Pattern:** `ab-002285` (`saas-error-handling.error-boundaries.unhandled-promise-rejections`)
- **Severity:** critical
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002285
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002285)

## Why it matters

Unhandled promise rejections crash Node.js server processes outright in newer runtimes, and silently swallow errors in the browser — making failures invisible to both users and monitoring. CWE-391 (Unchecked Error Condition) and CWE-755 (Improper Handling of Exceptional Conditions) both apply: async errors in event handlers and `useEffect` callbacks that lack `try/catch` or `.catch()` chains produce no user feedback and no diagnostic signal. ISO 25010 reliability.fault-tolerance requires that faults be contained — unguarded async paths let errors propagate with no containment boundary. This is especially acute in authentication and payment flows, where a silently failed async call leaves users in an ambiguous state.

## Severity rationale

Critical because unguarded async errors in Node.js crash server processes and in browsers produce silent failures with no user feedback or diagnostic signal.

## Remediation

Add a global safety net in your app entry point for any rejections that escape per-call handling, then add per-call `try/catch` in all async event handlers.

```ts
// In _app.tsx, main.tsx, or root layout.tsx (client component)
if (typeof window !== 'undefined') {
  window.addEventListener('unhandledrejection', (event) => {
    console.error('Unhandled rejection:', event.reason)
    // Report to your error monitoring service here
  })
}
```

For each async event handler, wrap the body:

```ts
async function handleSubmit() {
  try {
    await submitForm(data)
  } catch (error) {
    setErrorMessage('Submission failed. Please try again.')
    reportError(error)
  }
}
```

Verify by triggering a deliberate rejection in a development click handler and confirming it is caught rather than surfacing in the browser console unhandled.

## Detection

- **ID:** `unhandled-promise-rejections`
- **Severity:** `critical`
- **What to look for:** Focus analysis on these specific high-risk locations: (1) all event handler functions attached to UI elements (onClick, onSubmit, onChange handlers that are async or call async functions); (2) all `useEffect` callbacks that contain async operations or floating promises (async functions called without `await` inside useEffect); (3) all async functions in client components (`components/` and `app/` directories with 'use client'). Server components and Server Actions have framework-level error boundaries and are lower priority for this check. Also check for: a global `unhandledrejection` event listener in the app entry point (`main.tsx`, `_app.tsx`, root layout, or equivalent); `Promise.all()` / `Promise.allSettled()` calls without error handling in the above locations. Note: error boundaries do not catch async errors — they only catch synchronous render errors, so async errors need separate handling.
- **Pass criteria:** Count all async functions in event handlers, useEffect callbacks, and client components. Pass if ALL of the following are true: (a) at least 90% of async functions in event handlers, useEffect callbacks, and client component async functions consistently use `try/catch` or `.catch()`, OR (b) a global `window.addEventListener('unhandledrejection', ...)` handler is present in the entry point AND captures/reports the error. Report the ratio: "X of Y async client functions have error handling."
- **Fail criteria:** Fail if event handler functions (onClick, onSubmit, etc.) contain `await` calls without `try/catch`. Fail if data-fetching utilities throw without callers handling the rejection. Do not pass when a global unhandledrejection listener is present but more than 3 unguarded async calls exist in user-interaction paths — the global handler is a safety net, not a substitute for per-call handling.
- **Skip (N/A) when:** Never — all JavaScript runtimes (Node.js and browser) emit unhandled rejection events, making this applicable to every project regardless of framework.
- **Detail on fail:** Name specific files/functions where unguarded async calls were found (e.g., "handleSubmit in components/auth/login-form.tsx calls signIn() without try/catch; no global unhandledrejection listener found"). Max 500 chars.
- **Remediation:** Unhandled promise rejections crash server processes in Node.js and produce silent failures in browsers. Users see nothing; errors disappear.

  The simplest global safety net is a listener in your entry point:

  ```ts
  // In _app.tsx, main.tsx, or root layout.tsx (client component)
  if (typeof window !== 'undefined') {
    window.addEventListener('unhandledrejection', (event) => {
      console.error('Unhandled rejection:', event.reason)
      // Report to your error monitoring service here
    })
  }
  ```

  For individual async event handlers, always wrap with try/catch:

  ```ts
  async function handleSubmit() {
    try {
      await submitForm(data)
    } catch (error) {
      setErrorMessage('Submission failed. Please try again.')
      reportError(error) // send to monitoring
    }
  }
  ```

  After adding handlers, verify by triggering a deliberate rejection in development and confirming it's caught.

---

## External references

- cwe CWE-755
- cwe CWE-391
- iso-25010 reliability.fault-tolerance

Taxons: error-resilience, observability

HTML version: https://auditbuffet.com/patterns/ab-002285
