How do you handle errors in async code?
By FrontendPro Editorial Team Updated 9/26/2026
Answer
With async/await, ordinary try...catch works, because await turns a rejection back into a thrown error:
async function load() {
try {
const res = await fetch("/api/data");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
report(err);
throw err; // rethrow if the caller must know
}
}
Note the explicit res.ok check. fetch only rejects on network failure — a 404 or a 500 resolves normally, so without that line a server error slips through as success. This is the single most common async error-handling bug.
With promise chains
fetch("/api/data")
.then((r) => r.json())
.catch(handleError)
.finally(() => setLoading(false));
A .catch handles rejections from every step above it, so one at the end of the chain is usually enough.
Callbacks cannot be caught
A surrounding try cannot see an error thrown on a later tick:
try {
setTimeout(() => { throw new Error("lost"); });
} catch (e) { /* never runs */ }
Handle it inside the callback, or convert the API to a promise.
Several operations at once
await Promise.all([a(), b()]); // rejects on the first failure
await Promise.allSettled([a(), b()]); // waits for all, reports each outcome
Use allSettled when one failure should not discard the successful results.
Do not lose the context
Wrap with cause rather than replacing the error: throw new Error("Could not load user", { cause: err }).
Finally, add a global net — unhandledrejection in the browser, process.on("unhandledRejection") in Node. A missing await or a chain without .catch produces an unhandled rejection that no try will catch, and in Node that terminates the process.
