15 minlesson

Async Error Handling Patterns

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:

javascript
1// Sync: Error propagates naturally
2try {
3 throw new Error("Sync error");
4} catch (e) {
5 console.log("Caught:", e.message);
6}
7
8// 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()

javascript
1// Chain catches at the end
2fetchUser(1)
3 .then(user => fetchOrders(user.id))
4 .then(orders => processOrders(orders))
5 .catch(error => {
6 // Catches errors from ANY step
7 console.error("Error:", error.message);
8 });
9
10// Multiple catch for different handling
11fetchUser(1)
12 .catch(error => {
13 console.log("User fetch failed, using default");
14 return { id: 0, name: "Guest" }; // Recovery value
15 })
16 .then(user => fetchOrders(user.id))
17 .catch(error => {
18 console.log("Orders failed");
19 return []; // Recovery value
20 })
21 .then(orders => console.log("Orders:", orders));

Re-throwing Errors

javascript
1fetchData()
2 .catch(error => {
3 // Log but re-throw
4 console.error("Logging error:", error);
5 throw error; // Pass to next catch
6 })
7 .catch(error => {
8 // Handle for user
9 showErrorMessage(error.message);
10 });

Error Transformation

javascript
1class APIError extends Error {
2 constructor(message, status, code) {
3 super(message);
4 this.status = status;
5 this.code = code;
6 }
7}
8
9fetch("/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 error
23 if (error.status === 401) {
24 redirectToLogin();
25 }
26 }
27 throw error; // Re-throw unknown errors
28 });

Async/Await Error Handling

Basic try/catch

javascript
1async 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 default
9 }
10}

Handling Specific Errors

javascript
1async 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 issues
11 return retryPayment(orderId);
12 } else {
13 // Unknown error - log and re-throw
14 logError(error);
15 throw new Error("Payment failed. Please try again.");
16 }
17 }
18}

Error Handling Wrapper

javascript
1// Utility to wrap async functions
2function handleAsync(fn) {
3 return async (...args) => {
4 try {
5 return [await fn(...args), null];
6 } catch (error) {
7 return [null, error];
8 }
9 };
10}
11
12// Usage
13const safeFetch = handleAsync(async (url) => {
14 const response = await fetch(url);
15 return response.json();
16});
17
18const [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

javascript
1// If ANY promise rejects, .all() immediately rejects
2const promises = [
3 Promise.resolve(1),
4 Promise.reject(new Error("Fail")),
5 Promise.resolve(3)
6];
7
8try {
9 const results = await Promise.all(promises);
10} catch (error) {
11 // Immediately catches the rejection
12 // Other promises might still be pending!
13 console.log(error.message); // "Fail"
14}

Getting All Results Despite Errors

javascript
1// Use Promise.allSettled for all results
2const results = await Promise.allSettled(promises);
3
4results.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});
11
12// Or filter results
13const successful = results
14 .filter(r => r.status === "fulfilled")
15 .map(r => r.value);

Handling Partial Failures

javascript
1async function fetchAllUsers(ids) {
2 const results = await Promise.allSettled(
3 ids.map(id => fetchUser(id))
4 );
5
6 const users = [];
7 const errors = [];
8
9 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 });
16
17 if (errors.length > 0) {
18 console.warn(`Failed to fetch ${errors.length} users`);
19 }
20
21 return { users, errors };
22}

Retry Patterns

Simple Retry

javascript
1async 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

javascript
1async function fetchWithBackoff(url, maxRetries = 3) {
2 let lastError;
3
4 for (let attempt = 0; attempt < maxRetries; attempt++) {
5 try {
6 return await fetch(url);
7 } catch (error) {
8 lastError = error;
9
10 // Only retry on network errors
11 if (!isRetryable(error)) {
12 throw error;
13 }
14
15 // 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 }
21
22 throw lastError;
23}
24
25function isRetryable(error) {
26 return error.code === "NETWORK_ERROR" ||
27 error.code === "TIMEOUT" ||
28 error.status >= 500;
29}

Retry with Circuit Breaker

javascript
1class 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_OPEN
8 }
9
10 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 }
18
19 try {
20 const result = await fn();
21 this.onSuccess();
22 return result;
23 } catch (error) {
24 this.onFailure();
25 throw error;
26 }
27 }
28
29 onSuccess() {
30 this.failures = 0;
31 this.state = "CLOSED";
32 }
33
34 onFailure() {
35 this.failures++;
36 this.lastFailure = Date.now();
37 if (this.failures >= this.threshold) {
38 this.state = "OPEN";
39 }
40 }
41}
42
43// Usage
44const breaker = new CircuitBreaker();
45const data = await breaker.call(() => fetch("/api/data"));

Timeout Handling

javascript
1function withTimeout(promise, ms) {
2 const timeout = new Promise((_, reject) => {
3 setTimeout(() => reject(new Error("Timeout")), ms);
4 });
5 return Promise.race([promise, timeout]);
6}
7
8// Usage
9try {
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}
16
17// With AbortController (better - cancels the actual request)
18async function fetchWithTimeout(url, ms) {
19 const controller = new AbortController();
20 const timeoutId = setTimeout(() => controller.abort(), ms);
21
22 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

javascript
1// In browser
2window.addEventListener("unhandledrejection", (event) => {
3 console.error("Unhandled rejection:", event.reason);
4 // Prevent default error logging
5 event.preventDefault();
6 // Report to error tracking service
7 reportError(event.reason);
8});
9
10// In Node.js
11process.on("unhandledRejection", (reason, promise) => {
12 console.error("Unhandled rejection:", reason);
13});

Error Boundary Pattern

javascript
1class AsyncErrorBoundary {
2 constructor(onError) {
3 this.onError = onError;
4 }
5
6 async wrap(fn) {
7 try {
8 return await fn();
9 } catch (error) {
10 this.onError(error);
11 return null;
12 }
13 }
14}
15
16const boundary = new AsyncErrorBoundary((error) => {
17 console.error("Caught by boundary:", error);
18 showNotification("Something went wrong");
19});
20
21const result = await boundary.wrap(async () => {
22 const data = await fetchData();
23 return processData(data);
24});

Best Practices

  1. Always handle errors - Don't leave promises unhandled
  2. Be specific - Catch and handle specific error types
  3. Log appropriately - Log for debugging, show user-friendly messages
  4. Use finally - For cleanup that must always happen
  5. Fail gracefully - Provide fallbacks when possible
  6. Don't swallow errors - Re-throw unknown errors
  7. Use typed errors - Create error classes for different scenarios
javascript
1// Good error handling example
2async function loadUserDashboard(userId) {
3 const result = {
4 user: null,
5 posts: [],
6 notifications: [],
7 errors: []
8 };
9
10 // Load each independently
11 const [userResult, postsResult, notifResult] = await Promise.allSettled([
12 fetchUser(userId),
13 fetchUserPosts(userId),
14 fetchNotifications(userId)
15 ]);
16
17 if (userResult.status === "fulfilled") {
18 result.user = userResult.value;
19 } else {
20 result.errors.push({ type: "user", error: userResult.reason });
21 }
22
23 if (postsResult.status === "fulfilled") {
24 result.posts = postsResult.value;
25 } else {
26 result.errors.push({ type: "posts", error: postsResult.reason });
27 }
28
29 if (notifResult.status === "fulfilled") {
30 result.notifications = notifResult.value;
31 } else {
32 result.errors.push({ type: "notifications", error: notifResult.reason });
33 }
34
35 return result;
36}