How to Handle Unhandled Promise Rejections in Browsers

Unhandled promise rejections occur when an asynchronous operation fails within a JavaScript application without an attached error handler, often leading to broken UI states and degraded user experiences. In production environments, web development engineers capture, monitor, and mitigate these silent failures using browser-native event listeners, automated telemetry pipelines, robust linting rules, and centralized application performance monitoring (APM) tools. This guide details the technical mechanisms engineers use to identify these rejections across various browsers and neutralize them before they impact users.

The Global Detection Mechanism

Modern browsers provide a dedicated window-level event to intercept rejected promises that lack a local .catch() block or a surrounding try...catch statement. Engineers register an unhandledrejection event listener at the entry point of the application to centralize error capture:

window.addEventListener('unhandledrejection', (event) => {
  const { reason, promise } = event;

  // Prevent the default browser console error logging if desired
  event.preventDefault();

  // Extract critical debugging data
  const errorMessage = reason instanceof Error ? reason.message : String(reason);
  const errorStack = reason instanceof Error ? reason.stack : null;

  // Forward to a telemetry pipeline
  reportErrorToService({
    type: 'UnhandledPromiseRejection',
    message: errorMessage,
    stack: errorStack,
    timestamp: Date.now(),
  });
});

The event object exposes two critical properties: event.promise (the promise object that rejected) and event.reason (the value or error object passed to the rejection). Calling event.preventDefault() suppresses the default behavior of outputting an error message directly to the browser's developer console.

Handling Worker Contexts

Modern web applications frequently offload heavy computation or network synchronization to background threads. Because Web Workers and Service Workers run in isolated execution contexts, the top-level window object is unavailable.

Engineers monitor workers by attaching listeners to the global worker scope (self):

// Inside a Web Worker or Service Worker context
self.addEventListener('unhandledrejection', (event) => {
  // Transmit error details back to the main thread or directly to an API
  fetch('/api/logs/worker-error', {
    method: 'POST',
    body: JSON.stringify({
      workerType: self instanceof ServiceWorkerGlobalScope ? 'ServiceWorker' : 'WebWorker',
      reason: event.reason?.message || String(event.reason),
      stack: event.reason?.stack,
    }),
    headers: { 'Content-Type': 'application/json' },
  });
});

Addressing Rejection Tracking and Late Resolution

A promise rejection might technically be unhandled at the instant it fails, but handled later in the execution cycle. To prevent false alarms, browsers implement a paired event: rejectionhandled.

window.addEventListener('rejectionhandled', (event) => {
  // Notify telemetry that a previously logged rejection was handled late
  cancelReportedError(event.promise);
});

Production tracking libraries maintain an in-memory queue of unhandled promises with a brief debounce window (typically 50–100 milliseconds). If rejectionhandled fires for that specific promise instance within the window, the error report is discarded, preventing redundant bug tickets and noisy alerts.

Managing Cross-Origin Script Errors

When asynchronous code fails inside an external script loaded from a CDN or a different origin without proper Cross-Origin Resource Sharing (CORS) headers, browsers redact the error details to prevent information leakage. In these scenarios, event.reason may evaluate to generic strings like "Script error." with no usable stack trace.

Engineers resolve this issue by ensuring third-party assets meet two conditions:

  1. The script tag includes the crossorigin="anonymous" attribute.
  2. The server hosting the asset returns the Access-Control-Allow-Origin: * (or specific origin) response header.

Automated Reporting and Telemetry Architecture

Transmitting error payloads synchronously on every failure can degrade network performance or fail completely during browser termination. In production environments, engineers implement resilient delivery patterns:

  • Beacon API: For mission-critical telemetry that must reliably dispatch even if the user closes the tab immediately after an error, engineers use navigator.sendBeacon('/api/log', data).
  • Payload Batching: Non-fatal rejections are stored in memory and dispatched in batches periodically using idle browser callbacks such as requestIdleCallback() to minimize thread contention.
  • APM Integration: Teams integrate dedicated monitoring SDKs (such as Sentry, Datadog, or Bugsnag) which automatically wrap native promises, normalize stack traces across different browser engines (V8, Gecko, WebKit), and correlate failures with deployment versions and user session replays.

Prevention Strategies at Build Time

While detection is critical, high-performing teams apply preventative measures to prevent unhandled rejections from reaching production:

  • Strict Static Analysis: Enforcing ESLint rules such as @typescript-eslint/no-floating-promises requires developers to either await, .catch(), or explicitly return every promise instance.
  • Global Fallback Boundaries: In component-driven frameworks like React, Vue, or Angular, engineers route uncaught promise rejections into state managers or error boundaries to present graceful fallback UI rather than leaving the interface in a frozen or broken state.