Async Error Handling
Things go wrong. Networks fail. APIs break. Handle it gracefully.
Promise Errors
Rejections and .catch() — the Promise way to handle errors.
// A Promise can reject:
const promise = new Promise((resolve, reject) => {
reject(new Error("Something went wrong"));
});
// Or throw inside an executor:
const promise2 = new Promise(() => {
throw new Error("Oops"); // automatically rejects
});Promises reject when: you call reject(), an error is thrown inside the executor, or a .then() callback throws.
try/catch + async
The cleanest way to handle async errors.
async function loadUserData(userId) {
try {
const res = await fetch(`/api/users/${userId}`);
// Network succeeded but HTTP error:
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
}
const user = await res.json();
return user;
} catch (error) {
// Catches: network errors, HTTP errors, JSON errors
if (error.name === "TypeError") {
console.error("Network error:", error.message);
} else {
console.error("Request failed:", error.message);
}
return null;
} finally {
// Always runs — success or failure:
hideLoadingSpinner();
}
}try
happy path
catch
error path
finally
always runs
Error Propagation
Errors bubble up through async call chains.
// Errors propagate UP the call chain:
async function getUser(id) {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error("User not found");
return res.json();
}
async function getUserPosts(userId) {
const user = await getUser(userId); // can throw!
const res = await fetch(`/api/posts/${user.id}`);
return res.json();
}
async function displayDashboard(userId) {
try {
const posts = await getUserPosts(userId);
render(posts);
} catch (error) {
// Catches errors from getUser OR getUserPosts
showError(error.message);
}
}💡 You don't need try/catch in every function. Let errors propagate and catch them at the level where you can handle them meaningfully (show UI, retry, fallback).
Recovery Patterns
Graceful degradation when things fail.
// Pattern 1: Fallback value
async function getConfig() {
try {
const res = await fetch("/api/config");
return await res.json();
} catch {
return { theme: "light", lang: "en" }; // defaults
}
}
// Pattern 2: Retry logic
async function fetchWithRetry(url, attempts = 3) {
for (let i = 0; i < attempts; i++) {
try {
const res = await fetch(url);
if (res.ok) return await res.json();
throw new Error(`HTTP ${res.status}`);
} catch (err) {
if (i === attempts - 1) throw err; // give up
await delay(1000 * 2 ** i); // exponential backoff
}
}
}
// Pattern 3: Partial failure (load what you can)
async function loadDashboard() {
const [users, posts, stats] = await Promise.allSettled([
fetchUsers(),
fetchPosts(),
fetchStats()
]);
return {
users: users.status === "fulfilled" ? users.value : [],
posts: posts.status === "fulfilled" ? posts.value : [],
stats: stats.status === "fulfilled" ? stats.value : null
};
}Global Handlers
Catch unhandled errors as a safety net.
// Catch unhandled promise rejections (browser):
window.addEventListener("unhandledrejection", (event) => {
console.error("Unhandled rejection:", event.reason);
event.preventDefault(); // prevent default logging
// Send to error tracking service:
reportError(event.reason);
});
// Node.js:
process.on("unhandledRejection", (reason, promise) => {
console.error("Unhandled:", reason);
});
// These are SAFETY NETS, not replacements for
// proper error handling in your code!⚠️ Global handlers are for catching bugs you missed, not for normal error handling. Always use try/catch or .catch() for expected failures.
FAQ
Common questions about async error handling.