Async Error Handling (try/catch + Promise)
debt(d7/e3/b5/t7)
Closest to 'only careful code review or runtime testing' (d7). ESLint can flag some patterns (e.g. no-floating-promises with TypeScript plugin), but the core problem — a non-awaited Promise inside an async function or a fire-and-forget call — is not caught by default ESLint rules. TypeScript alone doesn't prevent it either. Unhandled rejections in browsers are silent; in Node <15 they were silent in production. Detection typically requires careful review or runtime monitoring.
Closest to 'simple parameterised fix' (e3). The quick_fix indicates wrapping async bodies in try/catch, adding .catch() to all chains, and adding a global unhandledRejection handler. This is a small but repeated pattern fix — not a single one-liner (e1), but not a cross-cutting refactor (e5+). Each missing await or missing .catch() is a local fix, but the pattern may need to be applied across multiple async functions.
Closest to 'persistent productivity tax' (b5). Async error handling applies broadly to both web and CLI contexts and affects every async operation in the codebase. Developers must consistently remember the pattern for every new async function they write, slowing down many work streams and requiring ongoing discipline, but it doesn't fundamentally reshape the architecture (not b7/b9).
Closest to 'serious trap' (t7). The misconception field states explicitly: developers believe try/catch in an async function catches all errors, but it doesn't catch errors in non-awaited Promises or in setTimeout callbacks. This contradicts the intuitive mental model of try/catch as a universal error boundary in synchronous code, and the 'obvious' pattern (wrap in try/catch and assume safety) is commonly wrong — aligning with a serious trap score.
TL;DR
Explanation
In async/await, try/catch catches errors from awaited Promises. But: errors in Promise chains without .catch() become unhandled rejections. async functions always return Promises — their errors are rejections, not synchronous throws. Common patterns: wrap entire async function in try/catch for simplicity, use .catch() on Promise chains, add global process.on('unhandledRejection') in Node as last resort. Parallel operations: await Promise.all() — if any rejects, the others still run but their results are lost. Use Promise.allSettled() to handle all outcomes.
Common Misconception
Why It Matters
Common Mistakes
- Not awaiting a Promise inside async function — rejection goes unhandled.
- Using try/catch but forgetting to await the operation.
- Not using Promise.allSettled() when you need all results regardless of failures.
Code Examples
async function loadData() {
fetch('/api/data'); // Not awaited — rejection unhandled
const result = await processData();
}
// Promise chain without .catch():
fetchUser().then(processUser); // Rejection unhandled
async function loadData() {
try {
const response = await fetch('/api/data');
const data = await response.json();
return data;
} catch (error) {
console.error('Load failed:', error);
throw error; // Re-throw for caller to handle
}
}
// Promise chain with .catch():
fetchUser()
.then(processUser)
.catch(err => logger.error(err));