JavaScript Async Programming Patterns

9 min read

JavaScript Async Programming Patterns

Asynchronous programming is fundamental to modern JavaScript. Let's explore different patterns and best practices.

From Callbacks to Promises

The Callback Hell Problem

// Callback hell - hard to read and maintain
fetchUser(userId, (user) => {
  fetchPosts(user.id, (posts) => {
    fetchComments(posts[0].id, (comments) => {
      console.log('Finally got comments:', comments)
    })
  })
})

Promises to the Rescue

// Much cleaner with promises
fetchUser(userId)
  .then(user => fetchPosts(user.id))
  .then(posts => fetchComments(posts[0].id))
  .then(comments => console.log('Comments:', comments))
  .catch(error => console.error('Error:', error))

Async/Await - The Modern Way

async function getUserData(userId) {
  try {
    const user = await fetchUser(userId)
    const posts = await fetchPosts(user.id)
    const comments = await fetchComments(posts[0].id)
    return { user, posts, comments }
  } catch (error) {
    console.error('Error fetching user data:', error)
    throw error
  }
}

Parallel Execution

Promise.all for Concurrent Operations

async function fetchAllData() {
  try {
    const [users, posts, categories] = await Promise.all([
      fetchUsers(),
      fetchPosts(),
      fetchCategories()
    ])
    
    return { users, posts, categories }
  } catch (error) {
    // If any promise fails, this catch block runs
    console.error('One or more requests failed:', error)
  }
}

Promise.allSettled for Fault Tolerance

async function fetchDataWithFallbacks() {
  const results = await Promise.allSettled([
    fetchCriticalData(),
    fetchOptionalData(),
    fetchAnalytics()
  ])
  
  const successful = results
    .filter(result => result.status === 'fulfilled')
    .map(result => result.value)
    
  const failed = results
    .filter(result => result.status === 'rejected')
    .map(result => result.reason)
    
  return { successful, failed }
}

Advanced Patterns

Retry Logic with Exponential Backoff

async function fetchWithRetry(url, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      const response = await fetch(url)
      if (response.ok) return response.json()
      throw new Error(`HTTP ${response.status}`)
    } catch (error) {
      if (i === maxRetries - 1) throw error
      
      // Exponential backoff
      const delay = Math.pow(2, i) * 1000
      await new Promise(resolve => setTimeout(resolve, delay))
    }
  }
}

Rate Limiting

class RateLimiter {
  constructor(maxRequests, timeWindow) {
    this.maxRequests = maxRequests
    this.timeWindow = timeWindow
    this.requests = []
  }
  
  async execute(fn) {
    const now = Date.now()
    
    // Remove old requests outside time window
    this.requests = this.requests.filter(
      time => now - time < this.timeWindow
    )
    
    if (this.requests.length >= this.maxRequests) {
      const oldestRequest = Math.min(...this.requests)
      const waitTime = this.timeWindow - (now - oldestRequest)
      await new Promise(resolve => setTimeout(resolve, waitTime))
    }
    
    this.requests.push(Date.now())
    return fn()
  }
}

Async Iterators

async function* fetchPages(baseUrl) {
  let page = 1
  let hasMore = true
  
  while (hasMore) {
    const response = await fetch(`${baseUrl}?page=${page}`)
    const data = await response.json()
    
    yield data.items
    
    hasMore = data.hasNext
    page++
  }
}

// Usage
for await (const items of fetchPages('/api/items')) {
  console.log('Processing page:', items)
}

Error Handling Best Practices

Global Error Handling

// Unhandled promise rejections
window.addEventListener('unhandledrejection', event => {
  console.error('Unhandled promise rejection:', event.reason)
  // Prevent default browser behavior
  event.preventDefault()
})

// Async function wrapper for better error handling
function asyncHandler(fn) {
  return async (...args) => {
    try {
      return await fn(...args)
    } catch (error) {
      console.error('Async error:', error)
      throw error
    }
  }
}

Custom Error Types

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

async function apiCall(url) {
  try {
    const response = await fetch(url)
    if (!response.ok) {
      throw new NetworkError(
        `Request failed: ${response.statusText}`,
        response.status
      )
    }
    return response.json()
  } catch (error) {
    if (error instanceof NetworkError) {
      // Handle network errors specifically
      console.error('Network error:', error.message, error.status)
    }
    throw error
  }
}

Conclusion

Mastering asynchronous JavaScript patterns is essential for building robust, performant applications. Use promises and async/await for clean code, implement proper error handling, and leverage advanced patterns like rate limiting and retry logic when needed.