Learning Objectives

  • Master try/catch with async/await
  • Handle multiple errors effectively
  • Implement error recovery strategies
  • Learn best practices for async error handling

Why Try/Catch with Async/Await?

Async/await allows you to use traditional try/catch blocks for error handling, making async code look and behave like synchronous code.

Promise .catch() vs Try/Catch

With Promises:

fetchUser(1)
  .then(user => fetchPosts(user.id))
  .then(posts => console.log(posts))
  .catch(error => console.error(error));

With Async/Await:

async function getUserPosts() {
  try {
    const user = await fetchUser(1);
    const posts = await fetchPosts(user.id);
    console.log(posts);
  } catch (error) {
    console.error(error);
  }
}

Basic Try/Catch Pattern

async function fetchData() {
  try {
    const response = await fetch('/api/data');
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Failed to fetch data:', error);
    throw error;
  }
}

Handling Multiple Operations

Sequential Operations

async function processOrder(orderId) {
  try {
    const order = await fetchOrder(orderId);
    const payment = await processPayment(order);
    const confirmation = await sendConfirmation(payment);
    return confirmation;
  } catch (error) {
    console.error('Order processing failed:', error);
    // Rollback logic here
    throw error;
  }
}

Parallel Operations

async function loadDashboard(userId) {
  try {
    const [user, posts, notifications] = await Promise.all([
      fetchUser(userId),
      fetchPosts(userId),
      fetchNotifications(userId)
    ]);
    
    return { user, posts, notifications };
  } catch (error) {
    console.error('Dashboard load failed:', error);
    throw error;
  }
}

Multiple Catch Blocks

Handle different error types with multiple try/catch blocks:

async function complexOperation() {
  let user;
  
  // Try to get user
  try {
    user = await fetchUser(1);
  } catch (error) {
    console.error('User fetch failed:', error);
    user = getDefaultUser();
  }
  
  // Try to get posts
  try {
    const posts = await fetchPosts(user.id);
    return { user, posts };
  } catch (error) {
    console.error('Posts fetch failed:', error);
    return { user, posts: [] };
  }
}

Error Recovery Patterns

1. Fallback Values

async function getUserData(userId) {
  try {
    return await fetchUser(userId);
  } catch (error) {
    console.warn('Using cached user data');
    return getCachedUser(userId);
  }
}

2. Retry Logic

async function fetchWithRetry(url, retries = 3) {
  for (let i = 0; i < retries; i++) {
    try {
      const response = await fetch(url);
      return await response.json();
    } catch (error) {
      if (i === retries - 1) throw error;
      console.log(`Retry ${i + 1}/${retries}`);
      await new Promise(resolve => setTimeout(resolve, 1000));
    }
  }
}

3. Graceful Degradation

async function loadPageData() {
  const data = {
    critical: null,
    optional: null
  };
  
  try {
    data.critical = await fetchCriticalData();
  } catch (error) {
    console.error('Critical data failed:', error);
    throw error; // Can't continue without critical data
  }
  
  try {
    data.optional = await fetchOptionalData();
  } catch (error) {
    console.warn('Optional data failed, continuing anyway');
    data.optional = null;
  }
  
  return data;
}

Finally Block

Use finally for cleanup operations that should run regardless of success or failure:

async function processFile(filename) {
  let file;
  
  try {
    file = await openFile(filename);
    const data = await readFile(file);
    await processData(data);
  } catch (error) {
    console.error('File processing failed:', error);
    throw error;
  } finally {
    if (file) {
      await closeFile(file);
      console.log('File closed');
    }
  }
}

Error Type Checking

class NetworkError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.name = 'NetworkError';
    this.statusCode = statusCode;
  }
}

async function fetchData(url) {
  try {
    const response = await fetch(url);
    
    if (!response.ok) {
      throw new NetworkError(
        'Request failed',
        response.status
      );
    }
    
    return await response.json();
  } catch (error) {
    if (error instanceof NetworkError) {
      if (error.statusCode === 404) {
        console.error('Resource not found');
      } else if (error.statusCode === 500) {
        console.error('Server error');
      }
    } else {
      console.error('Unexpected error:', error);
    }
    throw error;
  }
}

Real-World Example: Form Submission

async function submitForm(formData) {
  const submitButton = document.querySelector('#submit');
  const errorDiv = document.querySelector('#error');
  const successDiv = document.querySelector('#success');
  
  try {
    submitButton.disabled = true;
    errorDiv.textContent = '';
    
    // Validate
    await validateFormData(formData);
    
    // Submit
    const response = await fetch('/api/submit', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(formData)
    });
    
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    
    const result = await response.json();
    
    successDiv.textContent = 'Form submitted successfully!';
    return result;
    
  } catch (error) {
    errorDiv.textContent = `Error: ${error.message}`;
    console.error('Form submission failed:', error);
    throw error;
    
  } finally {
    submitButton.disabled = false;
  }
}

Common Mistakes

❌ Forgetting Try/Catch

// Bad: Unhandled rejection
async function bad() {
  const data = await fetchData(); // Can throw!
  return data;
}

// Good: Proper error handling
async function good() {
  try {
    const data = await fetchData();
    return data;
  } catch (error) {
    console.error('Error:', error);
    throw error;
  }
}

❌ Catching Without Re-throwing

// Bad: Silently swallows errors
async function bad() {
  try {
    return await fetchData();
  } catch (error) {
    console.error(error);
    // Error is lost!
  }
}

// Good: Re-throw or return fallback
async function good() {
  try {
    return await fetchData();
  } catch (error) {
    console.error(error);
    throw error; // Or return fallback
  }
}

❌ Not Using Finally for Cleanup

// Bad: Cleanup might not run
async function bad() {
  showSpinner();
  try {
    await fetchData();
    hideSpinner(); // Won't run if error!
  } catch (error) {
    hideSpinner(); // Duplicate code
    throw error;
  }
}

// Good: Finally ensures cleanup
async function good() {
  showSpinner();
  try {
    await fetchData();
  } catch (error) {
    console.error(error);
    throw error;
  } finally {
    hideSpinner(); // Always runs
  }
}

Best Practices

  1. Always use try/catch with await
  2. Be specific with error messages
  3. Use finally for cleanup
  4. Re-throw errors when appropriate
  5. Log errors for debugging
  6. Provide fallbacks when possible
  7. Don't swallow errors silently

Key Takeaways

  • ✅ Use try/catch for error handling with async/await
  • finally block always runs for cleanup
  • ✅ Multiple try/catch blocks for granular error handling
  • ✅ Re-throw errors to propagate them
  • ✅ Use custom error types for better error handling
  • ✅ Always handle errors - don't leave them unhandled

Next Steps

Next, we'll dive deeper into async functions and their advanced features!