
You’re building a fintech payments web app serving 5M+ monthly active users. A single unhandled async error in the checkout flow can cause failed payments, duplicate charges, or missing receipts—creating real revenue loss and regulatory risk. In modern JavaScript, most failures happen asynchronously (network timeouts, rejected Promises, event handlers), and error handling differs depending on whether you use callbacks, Promises, or async/await.
Explain how to handle errors in asynchronous JavaScript across the major async models. Your answer should cover:
try/catch around an async callback often doesn’t catch errors thrown inside the callback, and how errors should be surfaced instead..then() chains, the role of .catch(), and how to avoid “swallowed” errors.async/await: How await maps to Promise rejection, how try/catch works here, and patterns for handling partial failures (e.g., Promise.all vs Promise.allSettled).window.onerror, unhandledrejection, or Node’s process.on('unhandledRejection') as a safety net.Assume the interviewer expects staff-level depth: describe error propagation semantics, common pitfalls (missing return, forgetting await, mixing callbacks with Promises), and production patterns (timeouts, retries, cancellation/AbortController, error normalization). Include short code snippets to illustrate correct and incorrect patterns.
try/catch only catches exceptions thrown in the same synchronous call stack. Once control returns to the event loop (e.g., setTimeout, DOM events, I/O callbacks), a later throw happens on a different stack and won’t be caught by the earlier try/catch.
try {
setTimeout(() => { throw new Error('boom'); }, 0);
} catch (e) {
// Never runs
}
A thrown error inside a .then() handler becomes a rejected Promise. A rejected Promise stays rejected until handled by a .catch() (or a second argument to .then). Missing return in chains can break propagation and lead to unhandled rejections.
fetch(url)
.then(r => {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
})
.then(data => use(data))
.catch(err => report(err));
await p either yields the fulfilled value or throws the rejection reason as an exception. Therefore, try/catch around await is the idiomatic way to handle async errors, but only if you actually await (or return) the Promise you intend to guard.
async function pay() {
try {
const res = await fetch('/charge');
if (!res.ok) throw new Error('charge failed');
return await res.json();
} catch (e) {
// handle or rethrow
throw e;
}
}
Promise.all fails fast: the first rejection rejects the whole aggregate, which is great when you need all results or none. Promise.allSettled returns both successes and failures, useful for best-effort tasks like logging, analytics, or loading optional widgets.
const results = await Promise.allSettled([
fetch('/profile'),
fetch('/recommendations'),
]);
Browsers emit unhandledrejection and error events; Node emits unhandledRejection and uncaughtException. These are last-resort telemetry hooks, not primary control flow—production code should still handle errors locally and deterministically.
window.addEventListener('unhandledrejection', (e) => {
// log e.reason
});