Async Error Handling Patterns
Proper error handling in async code is crucial for building robust applications.
The Problem with Async Errors
Synchronous errors are straightforward:
javascript1// Sync: Error propagates naturally2try {3 throw new Error("Sync error");4} catch (e) {5 console.log("Caught:", e.message);6}78// Async: This WON'T catch the error!9try {10 setTimeout(() => {11 throw new Error("Async error"); // Uncaught!12 }, 100);13} catch (e) {14 console.log("Never reached");15}
Promise Error Handling
Using .catch()
javascript1// Chain catches at the end2fetchUser(1)3 .then(user => fetchOrders(user.id))4 .then(orders => processOrders(orders))5 .catch(error => {6 // Catches errors from ANY step7 console.error("Error:", error.message);8 });910// Multiple catch for different handling11fetchUser(1)12 .catch(error => {13 console.log("User fetch failed, using default");14 return { id: 0, name: "Guest" }; // Recovery value15 })16 .then(user => fetchOrders(user.id))17 .catch(error => {18 console.log("Orders failed");19 return []; // Recovery value20 })21 .then(orders => console.log("Orders:", orders));
Re-throwing Errors
javascript1fetchData()2 .catch(error => {3 // Log but re-throw4 console.error("Logging error:", error);5 throw error; // Pass to next catch6 })7 .catch(error => {8 // Handle for user9 showErrorMessage(error.message);10 });
Error Transformation
javascript1class APIError extends Error {2 constructor(message, status, code) {3 super(message);4 this.status = status;5 this.code = code;6 }7}89fetch("/api/data")10 .then(response => {11 if (!response.ok) {12 throw new APIError(13 "Request failed",14 response.status,15 response.status >= 500 ? "SERVER_ERROR" : "CLIENT_ERROR"16 );17 }18 return response.json();19 })20 .catch(error => {21 if (error instanceof APIError) {22 // Handle API-specific error23 if (error.status === 401) {24 redirectToLogin();25 }26 }27 throw error; // Re-throw unknown errors28 });
Async/Await Error Handling
Basic try/catch
javascript1async function fetchData() {2 try {3 const response = await fetch("/api/data");4 const data = await response.json();5 return data;6 } catch (error) {7 console.error("Error:", error);8 return null; // or throw, or return default9 }10}
Handling Specific Errors
javascript1async function processPayment(orderId) {2 try {3 const result = await chargeCard(orderId);4 return result;5 } catch (error) {6 if (error.code === "CARD_DECLINED") {7 notifyUser("Card declined. Please try another card.");8 throw error;9 } else if (error.code === "NETWORK_ERROR") {10 // Retry for network issues11 return retryPayment(orderId);12 } else {13 // Unknown error - log and re-throw14 logError(error);15 throw new Error("Payment failed. Please try again.");16 }17 }18}
Error Handling Wrapper
javascript1// Utility to wrap async functions2function handleAsync(fn) {3 return async (...args) => {4 try {5 return [await fn(...args), null];6 } catch (error) {7 return [null, error];8 }9 };10}1112// Usage13const safeFetch = handleAsync(async (url) => {14 const response = await fetch(url);15 return response.json();16});1718const [data, error] = await safeFetch("/api/data");19if (error) {20 console.log("Failed:", error.message);21} else {22 console.log("Success:", data);23}
Promise.all Error Handling
Fail-Fast Behavior
javascript1// If ANY promise rejects, .all() immediately rejects2const promises = [3 Promise.resolve(1),4 Promise.reject(new Error("Fail")),5 Promise.resolve(3)6];78try {9 const results = await Promise.all(promises);10} catch (error) {11 // Immediately catches the rejection12 // Other promises might still be pending!13 console.log(error.message); // "Fail"14}
Getting All Results Despite Errors
javascript1// Use Promise.allSettled for all results2const results = await Promise.allSettled(promises);34results.forEach((result, index) => {5 if (result.status === "fulfilled") {6 console.log(`Promise ${index} succeeded:`, result.value);7 } else {8 console.log(`Promise ${index} failed:`, result.reason);9 }10});1112// Or filter results13const successful = results14 .filter(r => r.status === "fulfilled")15 .map(r => r.value);
Handling Partial Failures
javascript1async function fetchAllUsers(ids) {2 const results = await Promise.allSettled(3 ids.map(id => fetchUser(id))4 );56 const users = [];7 const errors = [];89 results.forEach((result, i) => {10 if (result.status === "fulfilled") {11 users.push(result.value);12 } else {13 errors.push({ id: ids[i], error: result.reason });14 }15 });1617 if (errors.length > 0) {18 console.warn(`Failed to fetch ${errors.length} users`);19 }2021 return { users, errors };22}
Retry Patterns
Simple Retry
javascript1async function fetchWithRetry(url, retries = 3) {2 for (let attempt = 1; attempt <= retries; attempt++) {3 try {4 return await fetch(url);5 } catch (error) {6 if (attempt === retries) {7 throw error;8 }9 console.log(`Attempt ${attempt} failed, retrying...`);10 }11 }12}
Exponential Backoff
javascript1async function fetchWithBackoff(url, maxRetries = 3) {2 let lastError;34 for (let attempt = 0; attempt < maxRetries; attempt++) {5 try {6 return await fetch(url);7 } catch (error) {8 lastError = error;910 // Only retry on network errors11 if (!isRetryable(error)) {12 throw error;13 }1415 // Exponential backoff: 1s, 2s, 4s...16 const delay = Math.pow(2, attempt) * 1000;17 console.log(`Retrying in ${delay}ms...`);18 await sleep(delay);19 }20 }2122 throw lastError;23}2425function isRetryable(error) {26 return error.code === "NETWORK_ERROR" ||27 error.code === "TIMEOUT" ||28 error.status >= 500;29}
Retry with Circuit Breaker
javascript1class CircuitBreaker {2 constructor(threshold = 5, timeout = 60000) {3 this.failures = 0;4 this.threshold = threshold;5 this.timeout = timeout;6 this.lastFailure = null;7 this.state = "CLOSED"; // CLOSED, OPEN, HALF_OPEN8 }910 async call(fn) {11 if (this.state === "OPEN") {12 if (Date.now() - this.lastFailure > this.timeout) {13 this.state = "HALF_OPEN";14 } else {15 throw new Error("Circuit breaker is OPEN");16 }17 }1819 try {20 const result = await fn();21 this.onSuccess();22 return result;23 } catch (error) {24 this.onFailure();25 throw error;26 }27 }2829 onSuccess() {30 this.failures = 0;31 this.state = "CLOSED";32 }3334 onFailure() {35 this.failures++;36 this.lastFailure = Date.now();37 if (this.failures >= this.threshold) {38 this.state = "OPEN";39 }40 }41}4243// Usage44const breaker = new CircuitBreaker();45const data = await breaker.call(() => fetch("/api/data"));
Timeout Handling
javascript1function withTimeout(promise, ms) {2 const timeout = new Promise((_, reject) => {3 setTimeout(() => reject(new Error("Timeout")), ms);4 });5 return Promise.race([promise, timeout]);6}78// Usage9try {10 const data = await withTimeout(fetch("/api/slow"), 5000);11} catch (error) {12 if (error.message === "Timeout") {13 console.log("Request timed out");14 }15}1617// With AbortController (better - cancels the actual request)18async function fetchWithTimeout(url, ms) {19 const controller = new AbortController();20 const timeoutId = setTimeout(() => controller.abort(), ms);2122 try {23 const response = await fetch(url, { signal: controller.signal });24 return response;25 } finally {26 clearTimeout(timeoutId);27 }28}
Global Error Handling
Unhandled Rejections
javascript1// In browser2window.addEventListener("unhandledrejection", (event) => {3 console.error("Unhandled rejection:", event.reason);4 // Prevent default error logging5 event.preventDefault();6 // Report to error tracking service7 reportError(event.reason);8});910// In Node.js11process.on("unhandledRejection", (reason, promise) => {12 console.error("Unhandled rejection:", reason);13});
Error Boundary Pattern
javascript1class AsyncErrorBoundary {2 constructor(onError) {3 this.onError = onError;4 }56 async wrap(fn) {7 try {8 return await fn();9 } catch (error) {10 this.onError(error);11 return null;12 }13 }14}1516const boundary = new AsyncErrorBoundary((error) => {17 console.error("Caught by boundary:", error);18 showNotification("Something went wrong");19});2021const result = await boundary.wrap(async () => {22 const data = await fetchData();23 return processData(data);24});
Best Practices
- Always handle errors - Don't leave promises unhandled
- Be specific - Catch and handle specific error types
- Log appropriately - Log for debugging, show user-friendly messages
- Use finally - For cleanup that must always happen
- Fail gracefully - Provide fallbacks when possible
- Don't swallow errors - Re-throw unknown errors
- Use typed errors - Create error classes for different scenarios
javascript1// Good error handling example2async function loadUserDashboard(userId) {3 const result = {4 user: null,5 posts: [],6 notifications: [],7 errors: []8 };910 // Load each independently11 const [userResult, postsResult, notifResult] = await Promise.allSettled([12 fetchUser(userId),13 fetchUserPosts(userId),14 fetchNotifications(userId)15 ]);1617 if (userResult.status === "fulfilled") {18 result.user = userResult.value;19 } else {20 result.errors.push({ type: "user", error: userResult.reason });21 }2223 if (postsResult.status === "fulfilled") {24 result.posts = postsResult.value;25 } else {26 result.errors.push({ type: "posts", error: postsResult.reason });27 }2829 if (notifResult.status === "fulfilled") {30 result.notifications = notifResult.value;31 } else {32 result.errors.push({ type: "notifications", error: notifResult.reason });33 }3435 return result;36}