Async Error Handling
Errors in async code must be explicitly caught. Promise rejections without a .catch handler generate an unhandledRejection warning (and crash in Node 15+). In async/await, use try/catch blocks.
Async error handling = a safety net under a trapeze artist. No net (no .catch or try/catch) means the performer (rejection) falls and crashes the whole show. In Node 15+, unhandled rejections literally kill the process — the show stops.
A common pattern is wrapping each await in its own try/catch for fine-grained handling, or using a helper like async-safe wrappers: const [err, result] = await to(promise) that returns [error, null] or [null, value] similar to Go-style error handling. Always add a top-level .catch() to Promise chains, even if just for logging. In Express/Koa, unhandled async errors won't reach error middleware unless explicitly passed to next(err) — a frequent source of silent failures.
The unhandledRejection event fires when a rejection has no handler at the end of the current microtask queue drain. A rejection CAN be retroactively handled — if you call .catch() on a rejected promise after the fact, the unhandledRejection event fires but can be 'handled' by the rejectionHandled event. However, relying on this timing is fragile. In Node.js, using --unhandled-rejections=throw (default) or process.on('unhandledRejection') as a global safety net is essential for production. For structured error handling in complex systems, consider result types via libraries like neverthrow or creating discriminated union types with TypeScript.
Async errors must be explicitly caught — unhandled rejections crash Node.js processes in modern versions. try/catch works with async/await for centralized handling; .catch() chains work for Promise chains. The most insidious bug is a missing await before a Promise-returning call — the error lands in an unhandled rejection instead of the surrounding try/catch.
Returning a Promise from inside a try block without awaiting it means the try/catch will NOT catch its rejection — the rejection escapes to the outer scope. Always await Promises inside try blocks if you want their errors caught: try { await fetchData(); } not try { return fetchData(); }.