Back to Blog
Async2026-04-17

Promise Error Handling: A Practical Guide

Stop swallowing rejections. Patterns for clean, debuggable async error handling.

Promise errors are silent killers. A missed .catch becomes an unhandled rejection that may crash Node or bury bugs in the browser console.

Always Attach a Handler

Every promise chain needs either a .catch or a try/catch around its await:

``js fetch('/api').then(processData).catch(reportError); `

If the handler is far away, attach a noop catch immediately and store the promise: p.catch(()=>{}).

process.on('unhandledRejection') in Node

`js process.on('unhandledRejection', (reason, promise) => { console.error('Unhandled rejection', reason); // optional: process.exit(1); }); `

Node 15+ terminates the process by default on unhandled rejection. Always log them.

window.addEventListener('unhandledrejection')

In the browser, the same hook surfaces silent failures. Wire it to your error tracker.

Promise.allSettled vs Promise.all

Promise.all rejects on first failure, throwing away successes. When you need all results regardless:

`js const results = await Promise.allSettled(tasks); const successes = results.filter(r => r.status === 'fulfilled').map(r => r.value); const errors = results.filter(r => r.status === 'rejected').map(r => r.reason); `

Wrap Errors with Cause

ES2022 added Error cause:

`js try { await innerOp(); } catch (e) { throw new Error('User signup failed', { cause: e }); } `

The original stack is preserved. Modern loggers print the chain.

Custom Error Types

`js class HttpError extends Error { constructor(message, status) { super(message); this.status = status; } } `

if (e instanceof HttpError) beats parsing message strings.

Don't Swallow

try { await op(); } catch (_) {}` is the worst pattern. At minimum log; ideally rethrow or convert to a domain error.

For HTTP retry layered on top see [modern fetch API patterns](/blog/modern-fetch-api-patterns).