Asynchronous Programming Patterns

JavaScript offers various patterns for handling asynchronous operations. Understanding these patterns is crucial for writing efficient and maintainable async code.

Callbacks

// Basic callback pattern
function fetchData(callback) {
  setTimeout(() => {
    callback(null, { data: 'Success' });
  }, 1000);
}

// Callback hell example
fetchUser(function(err, user) {
  if (err) handleError(err);
  fetchProfile(user.id, function(err, profile) {
    if (err) handleError(err);
    fetchPosts(profile.id, function(err, posts) {
      if (err) handleError(err);
      // Deep nesting continues...
    });
  });
});

Promises

// Creating promises
const promise = new Promise((resolve, reject) => {
  if (success) {
    resolve('Success!');
  } else {
    reject(new Error('Failed'));
  }
});

// Promise chaining
fetchUser(userId)
  .then(user => fetchProfile(user.id))
  .then(profile => fetchPosts(profile.id))
  .catch(error => handleError(error));

// Promise.all for parallel execution
Promise.all([
  fetch('/api/users'),
  fetch('/api/posts'),
  fetch('/api/comments')
])
  .then(([users, posts, comments]) => {
    // Handle all responses
  });

// Promise.race for timeouts
Promise.race([
  fetch('/api/data'),
  new Promise((_, reject) => 
    setTimeout(() => reject(new Error('Timeout')), 5000)
  )
]);

Async/Await

// Basic async/await
async function fetchUserData() {
  try {
    const user = await fetchUser(userId);
    const profile = await fetchProfile(user.id);
    const posts = await fetchPosts(profile.id);
    return { user, profile, posts };
  } catch (error) {
    handleError(error);
  }
}

// Parallel execution with async/await
async function fetchAllData() {
  const [users, posts, comments] = await Promise.all([
    fetch('/api/users'),
    fetch('/api/posts'),
    fetch('/api/comments')
  ]);
  return { users, posts, comments };
}

// Error handling patterns
async function fetchWithRetry(url, retries = 3) {
  for (let i = 0; i < retries; i++) {
    try {
      return await fetch(url);
    } catch (error) {
      if (i === retries - 1) throw error;
      await new Promise(r => setTimeout(r, 1000 * Math.pow(2, i)));
    }
  }
}

Generators and Async Iterators

// Generator function
async function* createAsyncIterator() {
  for (let i = 0; i < 5; i++) {
    await new Promise(r => setTimeout(r, 1000));
    yield i;
  }
}

// Using async iterator
async function processItems() {
  for await (const item of createAsyncIterator()) {
    console.log(item);
  }
}

// Custom async iterator
const asyncIterable = {
  async *[Symbol.asyncIterator]() {
    yield 'Hello';
    yield 'Async';
    yield 'World';
  }
};

Common Interview Follow-up Questions

  1. How do you handle errors in async/await vs promises?
  2. What are the benefits of using async/await over promises?
  3. How do you implement timeout for async operations?
  4. When would you use generators over regular async functions?

Best Practices

  • Always handle errors in async operations
  • Use async/await for better readability
  • Implement proper timeout mechanisms
  • Consider parallel execution when possible
  • Use appropriate error retry strategies