Error handling in async JavaScript
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.
Two shapes
// 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
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
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:
try {
await fetch(url, { signal });
} catch (err) {
if (err.name === "AbortError") return; // not an actual failure
throw err;
}Error taxonomy
Real apps separate:
| Kind | Action |
|---|---|
| Network failure (offline, CORS, DNS) | Retry with backoff; offline UI. |
| HTTP 4xx | User-visible message; don't retry. |
| HTTP 5xx | Retry with backoff (transient). |
| HTTP 429 | Honor Retry-After. |
| AbortError | Silent (intentional cancel). |
| Validation | Surface near the input. |
Custom error classes help discriminate:
class HttpError extends Error {
constructor(status, body) { super(`HTTP ${status}`); this.status = status; this.body = body; }
}Don't swallow
try { ... } catch {} // SMELL — silently hides bugsIf you must catch and ignore, log it. If you catch and continue, comment why.
Re-throw or convert
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:
<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.