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.
Critical because unguarded async errors in Node.js crash server processes and in browsers produce silent failures with no user feedback or diagnostic signal.
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.
// 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:
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.
ID: saas-error-handling.error-boundaries.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:
// 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:
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.