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:
- The script tag includes the
crossorigin="anonymous"attribute. - 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-promisesrequires developers to eitherawait,.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.