Dataford
Interview Guides
Upgrade
All questions/Coding/Error Handling in Async JavaScript

Error Handling in Async JavaScript

Medium
Coding
Asked at 1 company1
Also asked at
TaskRabbit

Problem

Context

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.

Core Question

Explain how to handle errors in asynchronous JavaScript across the major async models. Your answer should cover:

  1. Callbacks & event loop: Why a try/catch around an async callback often doesn’t catch errors thrown inside the callback, and how errors should be surfaced instead.
  2. Promises: How rejections propagate through .then() chains, the role of .catch(), and how to avoid “swallowed” errors.
  3. 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).
  4. Global handling: When (and when not) to use window.onerror, unhandledrejection, or Node’s process.on('unhandledRejection') as a safety net.

Scope Guidance

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.

Key Concepts

Why try/catch fails across async boundaries

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
}

Promise rejection propagation and chaining

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));

async/await is syntactic sugar over Promises

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;
  }
}

Concurrency patterns: Promise.all vs allSettled

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'),
]);

Global safety nets for unhandled failures

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
});

Problem

Context

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.

Core Question

Explain how to handle errors in asynchronous JavaScript across the major async models. Your answer should cover:

  1. Callbacks & event loop: Why a try/catch around an async callback often doesn’t catch errors thrown inside the callback, and how errors should be surfaced instead.
  2. Promises: How rejections propagate through .then() chains, the role of .catch(), and how to avoid “swallowed” errors.
  3. 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).
  4. Global handling: When (and when not) to use window.onerror, unhandledrejection, or Node’s process.on('unhandledRejection') as a safety net.

Scope Guidance

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.

Key Concepts

Why try/catch fails across async boundaries

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
}

Promise rejection propagation and chaining

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));

async/await is syntactic sugar over Promises

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;
  }
}

Concurrency patterns: Promise.all vs allSettled

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'),
]);

Global safety nets for unhandled failures

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
});

Your answer
Try one AI text evaluation on us
Get structured feedback, scored against a 4-axis rubric. Premium unlocks unlimited.
0 wordstarget ~200
Up next
TaskRabbitValidate JavaScript Riddle AssertionsMedium
Next question