Loading...
Loading...
Things go wrong. Networks fail. APIs break. Handle it gracefully.
Browsers and servers spend most of their time waiting — for network responses, timers, and user input. Async JavaScript lets your program stay responsive while work happens in the background using callbacks, Promises, and async/await.
Learn Async Error Handling in JavaScript — covering Promise Errors, try/catch + async, Error Propagation, Recovery Patterns with interactive visual examples and step-by-step explanations.
Synchronous code runs one line at a time. But real applications wait for network requests, file reads, and timers. Async JavaScript lets your program keep running while that work happens in the background — without freezing the UI.
This module covers the full async story: callbacks, Promises, async/await, and the Fetch API. Visual timelines show when tasks enter the queue, when they resolve, and why async/await is syntactic sugar over Promises — not magic.
Understanding async error handling is essential for every JavaScript developer. It shows up in frontend UI code, backend APIs, and framework internals — and getting it wrong leads to bugs that are hard to trace. This chapter builds intuition with visuals so the behavior sticks.
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.
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
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).
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 }; }
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.
Common questions about async error handling.