Back to JavaScript
JavaScript
easy
mid

How do you handle errors in asynchronous JavaScript code?

Wrap awaits in try/catch or attach `.catch` at the call site. Unhandled rejections in browsers fire `unhandledrejection` event and console warning; in Node, eventually crash (since v15). Distinguish error kinds (network, 4xx, 5xx, AbortError). Use error boundaries (React) at UI level. Don't swallow errors with empty catch — escalate or convert to domain errors.

4 min read·~10 min to think through

Two shapes

js
// try/catch with await
try {
  const data = await fetchData();
  use(data);
} catch (err) {
  handle(err);
}

// .catch at call site
fetchData().then(use).catch(handle);

Both fine. try/catch reads better for sequential code.

Errors in async functions become rejections

js
async function f() { throw new Error("x"); }
f().then(null, console.error);   // logs "Error: x"

throw and return Promise.reject are equivalent in an async function.

Unhandled rejections

js
window.addEventListener("unhandledrejection", (event) => {
  console.error("unhandled rejection:", event.reason);
  event.preventDefault();    // suppress browser default logging
});

// Node:
process.on("unhandledRejection", (reason, promise) => { log(reason); });

Since Node 15, unhandled rejections crash the process by default. Browsers emit a console warning.

AbortError is not really an error

Cancellation throws AbortError. Treat it as expected:

js
try {
  await fetch(url, { signal });
} catch (err) {
  if (err.name === "AbortError") return;   // not an actual failure
  throw err;
}

Error taxonomy

Real apps separate:

KindAction
Network failure (offline, CORS, DNS)Retry with backoff; offline UI.
HTTP 4xxUser-visible message; don't retry.
HTTP 5xxRetry with backoff (transient).
HTTP 429Honor Retry-After.
AbortErrorSilent (intentional cancel).
ValidationSurface near the input.

Custom error classes help discriminate:

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

Don't swallow

js
try { ... } catch {}     // SMELL — silently hides bugs

If you must catch and ignore, log it. If you catch and continue, comment why.

Re-throw or convert

js
try { await api.save(); }
catch (err) {
  throw new DomainError("Failed to save profile", { cause: err });
}

ES2022 cause lets you preserve the original error.

React error boundaries

React class component (or third-party wrapper) catches render errors in its children. Pair with global handlers for everything else:

jsx
<ErrorBoundary fallback={<ErrorScreen />}>
  <Routes />
</ErrorBoundary>

Boundaries catch render errors — NOT async errors thrown outside render. For those, handle in the effect / action.

Logging

  • Send to Sentry / similar with stack + context (user, route, locale).
  • Tag releases for triage.
  • Sample noisy errors at high volume.

Common mistakes

  • Empty catch.
  • Missing try/catch around awaits with side effects.
  • Logging errors but rendering as if success.
  • Treating AbortError as a real failure.
  • Not exposing 4xx errors to users (they retry forever).

Interview framing

"Wrap awaits in try/catch or attach .catch at the call site. throw inside async = rejection. Hook unhandledrejection (browser) / unhandledRejection (Node) for last-resort logging. Distinguish error kinds — AbortError is intentional cancel and should be silently swallowed; network and 5xx are retryable; 4xx is user-facing; 429 needs Retry-After. Convert to domain errors with cause to preserve stack trace. React error boundaries catch render errors but NOT async errors in effects — handle those in the effect itself. Don't swallow with empty catch — log or re-throw."

Follow-up questions

  • How do you handle errors across boundaries (effect, render, async)?
  • What does the cause option do?
  • How would you distinguish transient vs permanent errors?

Common mistakes

  • Empty catch.
  • Treating AbortError as failure.
  • Forgetting React error boundaries don't catch async.
  • Logging but not surfacing.

Performance considerations

  • Throwing isn't expensive at normal frequency. Logging at scale is — sample.

Edge cases

  • Unhandled rejection crashing Node.
  • Errors during cleanup of useEffect.
  • Throwing inside an event handler.

Real-world examples

  • Sentry / Datadog APM, AWS SDK retry policy, React Query error states.

Senior engineer discussion

Seniors design an error taxonomy, distinguish handling per layer (effect, render, route), and instrument distinct error metrics rather than 'errors' as a single number.

Related questions