gtag('config', 'G-ZH2YLVEQLY');

Async/Await Error Handling - Complete Guide

Published: September 25, 2024 | Reading time: 20 minutes

Async/Await Error Handling Overview

Proper error handling in async/await code ensures robust applications:

Error Handling Benefits
# Error Handling Benefits
- Graceful error recovery
- Better user experience
- Debugging capabilities
- Application stability
- Error logging
- Monitoring integration
- Code maintainability

Basic Error Handling Patterns

Try-Catch with Async/Await

Basic Error Handling
# Basic Error Handling Patterns

# 1. Simple Try-Catch
async function fetchUserData(userId) {
  try {
    const response = await fetch(`/api/users/${userId}`);
    const userData = await response.json();
    return userData;
  } catch (error) {
    console.error('Error fetching user data:', error);
    throw error;
  }
}

# 2. Error Handling with Fallback
async function fetchUserDataWithFallback(userId) {
  try {
    const response = await fetch(`/api/users/${userId}`);
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    const userData = await response.json();
    return userData;
  } catch (error) {
    console.error('Error fetching user data:', error);
    // Return fallback data
    return {
      id: userId,
      name: 'Unknown User',
      email: 'unknown@example.com'
    };
  }
}

# 3. Multiple Async Operations
async function fetchUserAndPosts(userId) {
  try {
    const [userResponse, postsResponse] = await Promise.all([
      fetch(`/api/users/${userId}`),
      fetch(`/api/users/${userId}/posts`)
    ]);
    
    if (!userResponse.ok || !postsResponse.ok) {
      throw new Error('Failed to fetch user or posts');
    }
    
    const [user, posts] = await Promise.all([
      userResponse.json(),
      postsResponse.json()
    ]);
    
    return { user, posts };
  } catch (error) {
    console.error('Error fetching user and posts:', error);
    throw error;
  }
}

# 4. Sequential Operations with Error Handling
async function processUserData(userId) {
  try {
    const user = await fetchUserData(userId);
    const posts = await fetchUserPosts(user.id);
    const comments = await fetchUserComments(user.id);
    
    return {
      user,
      posts,
      comments
    };
  } catch (error) {
    console.error('Error processing user data:', error);
    throw error;
  }
}

# 5. Error Handling with Timeout
async function fetchWithTimeout(url, timeout = 5000) {
  try {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), timeout);
    
    const response = await fetch(url, {
      signal: controller.signal
    });
    
    clearTimeout(timeoutId);
    
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    
    return await response.json();
  } catch (error) {
    if (error.name === 'AbortError') {
      throw new Error('Request timeout');
    }
    throw error;
  }
}

# 6. Retry Logic with Error Handling
async function fetchWithRetry(url, maxRetries = 3) {
  let lastError;
  
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch(url);
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      return await response.json();
    } catch (error) {
      lastError = error;
      console.log(`Attempt ${attempt} failed:`, error.message);
      
      if (attempt < maxRetries) {
        const delay = Math.pow(2, attempt) * 1000; // Exponential backoff
        await new Promise(resolve => setTimeout(resolve, delay));
      }
    }
  }
  
  throw new Error(`Failed after ${maxRetries} attempts: ${lastError.message}`);
}

# 7. Error Handling with Validation
async function createUser(userData) {
  try {
    // Validate input
    if (!userData.email || !userData.name) {
      throw new Error('Email and name are required');
    }
    
    const response = await fetch('/api/users', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(userData)
    });
    
    if (!response.ok) {
      const errorData = await response.json();
      throw new Error(errorData.message || 'Failed to create user');
    }
    
    return await response.json();
  } catch (error) {
    console.error('Error creating user:', error);
    throw error;
  }
}

# 8. Error Handling with Cleanup
async function processFile(filePath) {
  let fileHandle;
  
  try {
    fileHandle = await fs.open(filePath, 'r');
    const content = await fileHandle.readFile('utf8');
    
    // Process content
    const processedContent = await processContent(content);
    
    return processedContent;
  } catch (error) {
    console.error('Error processing file:', error);
    throw error;
  } finally {
    if (fileHandle) {
      await fileHandle.close();
    }
  }
}

# 9. Error Handling with Logging
async function apiCall(endpoint, data) {
  try {
    const response = await fetch(endpoint, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(data)
    });
    
    if (!response.ok) {
      throw new Error(`API call failed: ${response.status}`);
    }
    
    const result = await response.json();
    
    // Log success
    console.log(`API call successful: ${endpoint}`);
    
    return result;
  } catch (error) {
    // Log error with context
    console.error(`API call failed: ${endpoint}`, {
      error: error.message,
      data,
      timestamp: new Date().toISOString()
    });
    
    throw error;
  }
}

# 10. Error Handling with Custom Error Types
class APIError extends Error {
  constructor(message, status, endpoint) {
    super(message);
    this.name = 'APIError';
    this.status = status;
    this.endpoint = endpoint;
  }
}

async function fetchData(endpoint) {
  try {
    const response = await fetch(endpoint);
    
    if (!response.ok) {
      throw new APIError(
        `Request failed with status ${response.status}`,
        response.status,
        endpoint
      );
    }
    
    return await response.json();
  } catch (error) {
    if (error instanceof APIError) {
      console.error(`API Error: ${error.endpoint} - ${error.message}`);
    } else {
      console.error('Unexpected error:', error);
    }
    
    throw error;
  }
}

Advanced Error Handling Patterns

Complex Error Scenarios

Advanced Error Handling
# Advanced Error Handling Patterns

# 1. Error Boundary Pattern
class AsyncErrorBoundary {
  constructor() {
    this.errorHandlers = new Map();
  }
  
  registerHandler(errorType, handler) {
    this.errorHandlers.set(errorType, handler);
  }
  
  async execute(operation) {
    try {
      return await operation();
    } catch (error) {
      const handler = this.errorHandlers.get(error.constructor);
      if (handler) {
        return await handler(error);
      }
      throw error;
    }
  }
}

# 2. Circuit Breaker Pattern
class CircuitBreaker {
  constructor(threshold = 5, timeout = 60000) {
    this.threshold = threshold;
    this.timeout = timeout;
    this.failureCount = 0;
    this.lastFailureTime = null;
    this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
  }
  
  async execute(operation) {
    if (this.state === 'OPEN') {
      if (Date.now() - this.lastFailureTime > this.timeout) {
        this.state = 'HALF_OPEN';
      } else {
        throw new Error('Circuit breaker is OPEN');
      }
    }
    
    try {
      const result = await operation();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }
  
  onSuccess() {
    this.failureCount = 0;
    this.state = 'CLOSED';
  }
  
  onFailure() {
    this.failureCount++;
    this.lastFailureTime = Date.now();
    
    if (this.failureCount >= this.threshold) {
      this.state = 'OPEN';
    }
  }
}

# 3. Error Aggregation Pattern
async function executeMultipleOperations(operations) {
  const results = [];
  const errors = [];
  
  for (const operation of operations) {
    try {
      const result = await operation();
      results.push({ success: true, data: result });
    } catch (error) {
      errors.push(error);
      results.push({ success: false, error: error.message });
    }
  }
  
  if (errors.length > 0) {
    console.error('Multiple operations failed:', errors);
  }
  
  return results;
}

# 4. Error Recovery Pattern
async function resilientOperation(operation, fallback) {
  try {
    return await operation();
  } catch (error) {
    console.error('Primary operation failed:', error);
    
    try {
      console.log('Attempting fallback operation...');
      return await fallback();
    } catch (fallbackError) {
      console.error('Fallback operation also failed:', fallbackError);
      throw new Error(`Both primary and fallback operations failed: ${error.message}, ${fallbackError.message}`);
    }
  }
}

# 5. Error Classification Pattern
class ErrorClassifier {
  static classify(error) {
    if (error.name === 'TypeError') {
      return 'VALIDATION_ERROR';
    } else if (error.message.includes('timeout')) {
      return 'TIMEOUT_ERROR';
    } else if (error.message.includes('network')) {
      return 'NETWORK_ERROR';
    } else if (error.message.includes('permission')) {
      return 'PERMISSION_ERROR';
    } else {
      return 'UNKNOWN_ERROR';
    }
  }
  
  static async handleError(error) {
    const errorType = this.classify(error);
    
    switch (errorType) {
      case 'VALIDATION_ERROR':
        return { retry: false, message: 'Invalid input provided' };
      case 'TIMEOUT_ERROR':
        return { retry: true, message: 'Operation timed out' };
      case 'NETWORK_ERROR':
        return { retry: true, message: 'Network connection failed' };
      case 'PERMISSION_ERROR':
        return { retry: false, message: 'Insufficient permissions' };
      default:
        return { retry: false, message: 'An unexpected error occurred' };
    }
  }
}

# 6. Error Context Pattern
class ErrorContext {
  constructor() {
    this.context = new Map();
  }
  
  set(key, value) {
    this.context.set(key, value);
  }
  
  get(key) {
    return this.context.get(key);
  }
  
  async execute(operation) {
    try {
      return await operation();
    } catch (error) {
      // Add context to error
      error.context = Object.fromEntries(this.context);
      throw error;
    }
  }
}

# 7. Error Monitoring Pattern
class ErrorMonitor {
  constructor() {
    this.errors = [];
    this.maxErrors = 100;
  }
  
  logError(error, context = {}) {
    const errorEntry = {
      timestamp: new Date().toISOString(),
      message: error.message,
      stack: error.stack,
      context
    };
    
    this.errors.push(errorEntry);
    
    if (this.errors.length > this.maxErrors) {
      this.errors.shift();
    }
    
    // Send to monitoring service
    this.sendToMonitoring(errorEntry);
  }
  
  sendToMonitoring(errorEntry) {
    // Implementation depends on monitoring service
    console.log('Sending error to monitoring:', errorEntry);
  }
  
  getErrorStats() {
    const errorTypes = {};
    this.errors.forEach(error => {
      const type = error.message.split(':')[0];
      errorTypes[type] = (errorTypes[type] || 0) + 1;
    });
    
    return {
      totalErrors: this.errors.length,
      errorTypes,
      recentErrors: this.errors.slice(-10)
    };
  }
}

# 8. Error Recovery Strategies
class ErrorRecovery {
  static async retryWithBackoff(operation, maxRetries = 3) {
    let lastError;
    
    for (let attempt = 1; attempt <= maxRetries; attempt++) {
      try {
        return await operation();
      } catch (error) {
        lastError = error;
        
        if (attempt < maxRetries) {
          const delay = Math.pow(2, attempt) * 1000;
          await new Promise(resolve => setTimeout(resolve, delay));
        }
      }
    }
    
    throw lastError;
  }
  
  static async retryWithExponentialBackoff(operation, maxRetries = 3) {
    let lastError;
    
    for (let attempt = 1; attempt <= maxRetries; attempt++) {
      try {
        return await operation();
      } catch (error) {
        lastError = error;
        
        if (attempt < maxRetries) {
          const delay = Math.pow(2, attempt) * 1000 + Math.random() * 1000;
          await new Promise(resolve => setTimeout(resolve, delay));
        }
      }
    }
    
    throw lastError;
  }
  
  static async retryWithJitter(operation, maxRetries = 3) {
    let lastError;
    
    for (let attempt = 1; attempt <= maxRetries; attempt++) {
      try {
        return await operation();
      } catch (error) {
        lastError = error;
        
        if (attempt < maxRetries) {
          const baseDelay = 1000;
          const jitter = Math.random() * 1000;
          const delay = baseDelay * attempt + jitter;
          await new Promise(resolve => setTimeout(resolve, delay));
        }
      }
    }
    
    throw lastError;
  }
}

Error Handling Best Practices

Guidelines and Patterns

Best Practices

  • Always use try-catch with async/await
  • Handle specific error types
  • Implement proper logging
  • Use error boundaries
  • Implement retry logic
  • Provide fallback mechanisms
  • Monitor error patterns

Common Mistakes

  • Forgetting try-catch blocks
  • Not handling promise rejections
  • Swallowing errors silently
  • Poor error messages
  • Not implementing retries
  • Missing error context
  • Inadequate error monitoring

Production Error Handling

Real-world Implementation

Production Error Handling
# Production Error Handling

# 1. Express.js Error Handling
const express = require('express');
const app = express();

// Error handling middleware
app.use(async (error, req, res, next) => {
  console.error('Unhandled error:', error);
  
  // Log error to monitoring service
  await logError(error, {
    url: req.url,
    method: req.method,
    userAgent: req.get('User-Agent'),
    ip: req.ip
  });
  
  // Send appropriate response
  if (error.name === 'ValidationError') {
    res.status(400).json({
      error: 'Validation failed',
      message: error.message
    });
  } else if (error.name === 'UnauthorizedError') {
    res.status(401).json({
      error: 'Unauthorized',
      message: 'Authentication required'
    });
  } else {
    res.status(500).json({
      error: 'Internal server error',
      message: 'Something went wrong'
    });
  }
});

# 2. Global Error Handler
process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled Rejection at:', promise, 'reason:', reason);
  
  // Log to monitoring service
  logError(reason, {
    type: 'unhandledRejection',
    promise: promise.toString()
  });
});

process.on('uncaughtException', (error) => {
  console.error('Uncaught Exception:', error);
  
  // Log to monitoring service
  logError(error, {
    type: 'uncaughtException'
  });
  
  // Graceful shutdown
  process.exit(1);
});

# 3. Error Logging Service
class ErrorLoggingService {
  constructor() {
    this.logs = [];
  }
  
  async logError(error, context = {}) {
    const errorLog = {
      timestamp: new Date().toISOString(),
      message: error.message,
      stack: error.stack,
      name: error.name,
      context
    };
    
    this.logs.push(errorLog);
    
    // Send to external service
    await this.sendToExternalService(errorLog);
  }
  
  async sendToExternalService(errorLog) {
    try {
      await fetch('https://api.logging-service.com/errors', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${process.env.LOGGING_API_KEY}`
        },
        body: JSON.stringify(errorLog)
      });
    } catch (error) {
      console.error('Failed to send error to external service:', error);
    }
  }
}

# 4. Error Rate Limiting
class ErrorRateLimiter {
  constructor(maxErrors = 10, windowMs = 60000) {
    this.maxErrors = maxErrors;
    this.windowMs = windowMs;
    this.errors = [];
  }
  
  canLogError() {
    const now = Date.now();
    this.errors = this.errors.filter(time => now - time < this.windowMs);
    
    return this.errors.length < this.maxErrors;
  }
  
  logError() {
    this.errors.push(Date.now());
  }
}

# 5. Error Alerting
class ErrorAlerting {
  constructor() {
    this.alertThresholds = {
      errorRate: 10, // errors per minute
      errorCount: 100 // total errors
    };
    this.alertsSent = new Set();
  }
  
  async checkAndAlert(errorStats) {
    const { errorRate, errorCount } = errorStats;
    
    if (errorRate > this.alertThresholds.errorRate) {
      await this.sendAlert('HIGH_ERROR_RATE', {
        errorRate,
        threshold: this.alertThresholds.errorRate
      });
    }
    
    if (errorCount > this.alertThresholds.errorCount) {
      await this.sendAlert('HIGH_ERROR_COUNT', {
        errorCount,
        threshold: this.alertThresholds.errorCount
      });
    }
  }
  
  async sendAlert(type, data) {
    const alertKey = `${type}_${Date.now()}`;
    
    if (this.alertsSent.has(alertKey)) {
      return; // Prevent duplicate alerts
    }
    
    this.alertsSent.add(alertKey);
    
    // Send alert to monitoring service
    console.log(`Alert: ${type}`, data);
  }
}

Summary

Async/await error handling involves several key components:

  • Basic Patterns: Try-catch blocks, error propagation, and fallback mechanisms
  • Advanced Patterns: Error boundaries, circuit breakers, and error classification
  • Best Practices: Guidelines and common mistakes to avoid
  • Production Implementation: Real-world error handling and monitoring

Need More Help?

Struggling with async/await error handling or need help implementing robust error management? Our JavaScript experts can help you build resilient applications.

Get Error Handling Help