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.