⏳ Asynchronous JavaScript

JavaScript is single-threaded, meaning it executes one operation at a time. Asynchronous programming allows you to perform long-running operations (like fetching data, reading files, or waiting for timers) without blocking the execution of other code.

💡 Key Concept

Asynchronous code allows JavaScript to handle time-consuming tasks in the background while continuing to run other code. This is essential for building responsive web applications.

🔄 Synchronous vs Asynchronous Code

Synchronous Code (Blocking)
// Synchronous - executes line by line
console.log('First');
console.log('Second');
console.log('Third');

// Output (in order):
// First
// Second
// Third

// Blocking example
function slowOperation() {
  // This blocks execution for 3 seconds
  const start = Date.now();
  while (Date.now() - start < 3000) {}
  return 'Done';
}

console.log('Start');
const result = slowOperation();  // Blocks here!
console.log(result);
console.log('End');
Asynchronous Code (Non-Blocking)
// Asynchronous - doesn't wait
console.log('First');

setTimeout(() => {
  console.log('Second (after 2 seconds)');
}, 2000);

console.log('Third');

// Output:
// First
// Third
// Second (after 2 seconds)

// Non-blocking example
console.log('Start');

setTimeout(() => {
  console.log('This runs after 1 second');
}, 1000);

console.log('End (runs immediately)');

// Output:
// Start
// End (runs immediately)
// This runs after 1 second

📞 Callbacks

A callback is a function passed as an argument to another function, to be executed later when an asynchronous operation completes.

Callback Functions
// Simple callback example
function greet(name, callback) {
  console.log('Hello, ' + name);
  callback();
}

greet('John', function() {
  console.log('Callback executed!');
});

// Asynchronous callback
function fetchUserData(userId, callback) {
  console.log('Fetching user data...');

  setTimeout(() => {
    const user = {
      id: userId,
      name: 'John Doe',
      email: 'john@example.com'
    };
    callback(user);
  }, 2000);
}

fetchUserData(1, function(user) {
  console.log('User data received:', user);
});

// Callback with error handling
function fetchData(url, onSuccess, onError) {
  setTimeout(() => {
    const success = Math.random() > 0.5;

    if (success) {
      onSuccess({ data: 'Some data' });
    } else {
      onError(new Error('Failed to fetch data'));
    }
  }, 1000);
}

fetchData(
  '/api/users',
  (data) => console.log('Success:', data),
  (error) => console.error('Error:', error)
);

Callback Hell (Pyramid of Doom)

Callback Hell - Nested Callbacks
// ❌ Bad: Callback hell (hard to read and maintain)
fetchUser(userId, (user) => {
  fetchPosts(user.id, (posts) => {
    fetchComments(posts[0].id, (comments) => {
      fetchReplies(comments[0].id, (replies) => {
        console.log('Finally got replies:', replies);
      }, (error) => {
        console.error('Error fetching replies:', error);
      });
    }, (error) => {
      console.error('Error fetching comments:', error);
    });
  }, (error) => {
    console.error('Error fetching posts:', error);
  });
}, (error) => {
  console.error('Error fetching user:', error);
});

// This is hard to:
// - Read and understand
// - Handle errors properly
// - Maintain and debug
⚠️ Callback Hell

Deeply nested callbacks become difficult to read and maintain. This is why Promises and async/await were introduced.

🤝 Promises

A Promise is an object representing the eventual completion (or failure) of an asynchronous operation. It's a cleaner way to handle async code than callbacks.

Promise States

📝 Three States of a Promise
  • Pending - Initial state, operation is ongoing
  • Fulfilled - Operation completed successfully
  • Rejected - Operation failed with an error

Creating Promises

Promise Syntax
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
  // Asynchronous operation here

  const success = true;

  if (success) {
    resolve('Operation successful!');  // Fulfill the promise
  } else {
    reject('Operation failed!');       // Reject the promise
  }
});

// Basic promise example
const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('Data fetched successfully!');
  }, 2000);
});

// Consuming the promise
promise.then((result) => {
  console.log(result);  // "Data fetched successfully!"
});

// Promise with error
const failingPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject(new Error('Something went wrong!'));
  }, 1000);
});

failingPromise.catch((error) => {
  console.error('Error:', error.message);
});

Using Promises (.then() and .catch())

Promise Methods
// .then() - handles successful resolution
promise.then((result) => {
  console.log('Success:', result);
});

// .catch() - handles rejection
promise.catch((error) => {
  console.error('Error:', error);
});

// .finally() - runs regardless of outcome
promise.finally(() => {
  console.log('Promise completed (success or failure)');
});

// Chaining all together
fetchData()
  .then((data) => {
    console.log('Data:', data);
    return data;
  })
  .catch((error) => {
    console.error('Error:', error);
  })
  .finally(() => {
    console.log('Cleanup or loading indicator removal');
  });

// Practical example
function fetchUserData(userId) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (userId > 0) {
        resolve({
          id: userId,
          name: 'John Doe',
          email: 'john@example.com'
        });
      } else {
        reject(new Error('Invalid user ID'));
      }
    }, 1000);
  });
}

fetchUserData(1)
  .then((user) => {
    console.log('User:', user);
  })
  .catch((error) => {
    console.error('Error:', error.message);
  });

Promise Chaining

Chaining Promises
// ✅ Good: Promise chaining (much cleaner than callbacks)
fetchUser(userId)
  .then((user) => {
    console.log('User:', user);
    return fetchPosts(user.id);  // Return next promise
  })
  .then((posts) => {
    console.log('Posts:', posts);
    return fetchComments(posts[0].id);
  })
  .then((comments) => {
    console.log('Comments:', comments);
    return fetchReplies(comments[0].id);
  })
  .then((replies) => {
    console.log('Replies:', replies);
  })
  .catch((error) => {
    // Single error handler for entire chain
    console.error('Error at any step:', error);
  });

// Transforming data through chain
fetchUserData(1)
  .then((user) => {
    return user.name;  // Extract name
  })
  .then((name) => {
    return name.toUpperCase();  // Transform
  })
  .then((upperName) => {
    console.log('Upper name:', upperName);
  })
  .catch((error) => {
    console.error('Error:', error);
  });

// Multiple operations
getUserId()
  .then((id) => fetchUser(id))
  .then((user) => fetchOrders(user.id))
  .then((orders) => processOrders(orders))
  .then((result) => console.log('Final result:', result))
  .catch((error) => console.error('Error:', error));

⚡ Async/Await

async/await is modern syntax that makes asynchronous code look and behave more like synchronous code. It's built on top of Promises and makes code much more readable.

Async Functions

async/await Syntax
// async function always returns a Promise
async function myFunction() {
  return 'Hello';
}

// Equivalent to:
function myFunction() {
  return Promise.resolve('Hello');
}

// Using the async function
myFunction().then((result) => {
  console.log(result);  // "Hello"
});

// await keyword (only works inside async functions)
async function fetchData() {
  const data = await someAsyncOperation();
  console.log(data);  // Wait for promise to resolve
}

// Basic example
async function getUserData() {
  const response = await fetchUserData(1);
  console.log('User:', response);
  return response;
}

getUserData();

await - Waiting for Promises

Using await
// Without async/await (Promise chain)
function getUserInfo() {
  fetchUser(1)
    .then((user) => {
      console.log('User:', user);
      return fetchPosts(user.id);
    })
    .then((posts) => {
      console.log('Posts:', posts);
    })
    .catch((error) => {
      console.error('Error:', error);
    });
}

// ✅ With async/await (much cleaner!)
async function getUserInfo() {
  try {
    const user = await fetchUser(1);
    console.log('User:', user);

    const posts = await fetchPosts(user.id);
    console.log('Posts:', posts);
  } catch (error) {
    console.error('Error:', error);
  }
}

// Multiple sequential operations
async function processUserData() {
  const user = await fetchUser(1);
  const posts = await fetchPosts(user.id);
  const comments = await fetchComments(posts[0].id);
  const replies = await fetchReplies(comments[0].id);

  console.log('Got all data:', { user, posts, comments, replies });
}

// Returning values
async function getUsername(userId) {
  const user = await fetchUser(userId);
  return user.name;  // Automatically wrapped in Promise
}

const name = await getUsername(1);  // Can use await if in async context
console.log(name);

Error Handling with try/catch

try/catch with async/await
// Error handling with try/catch
async function fetchData() {
  try {
    const data = await someAsyncOperation();
    console.log('Data:', data);
    return data;
  } catch (error) {
    console.error('Error occurred:', error.message);
    throw error;  // Re-throw if needed
  }
}

// Multiple try/catch blocks
async function processData() {
  let user;
  let posts;

  try {
    user = await fetchUser(1);
  } catch (error) {
    console.error('Failed to fetch user:', error);
    return;  // Exit early
  }

  try {
    posts = await fetchPosts(user.id);
  } catch (error) {
    console.error('Failed to fetch posts:', error);
    posts = [];  // Use default value
  }

  return { user, posts };
}

// finally block
async function fetchWithCleanup() {
  try {
    showLoadingSpinner();
    const data = await fetchData();
    return data;
  } catch (error) {
    console.error('Error:', error);
    showErrorMessage(error.message);
  } finally {
    hideLoadingSpinner();  // Always runs
  }
}

// Handling specific errors
async function smartFetch() {
  try {
    const data = await fetchData();
    return data;
  } catch (error) {
    if (error.message.includes('404')) {
      console.log('Resource not found');
    } else if (error.message.includes('500')) {
      console.log('Server error');
    } else {
      console.log('Unknown error:', error);
    }
    throw error;
  }
}

🔀 Promise Methods

Promise.all() - Wait for All

Promise.all()
// Promise.all() - waits for ALL promises to resolve
const promise1 = fetchUser(1);
const promise2 = fetchPosts(1);
const promise3 = fetchComments(1);

Promise.all([promise1, promise2, promise3])
  .then(([user, posts, comments]) => {
    console.log('All resolved:', { user, posts, comments });
  })
  .catch((error) => {
    console.error('At least one failed:', error);
  });

// With async/await
async function fetchAllData() {
  try {
    const [user, posts, comments] = await Promise.all([
      fetchUser(1),
      fetchPosts(1),
      fetchComments(1)
    ]);

    console.log('User:', user);
    console.log('Posts:', posts);
    console.log('Comments:', comments);
  } catch (error) {
    console.error('One or more requests failed:', error);
  }
}

// Practical example: Parallel API calls
async function getDashboardData() {
  const [user, stats, notifications, messages] = await Promise.all([
    fetchUser(),
    fetchStats(),
    fetchNotifications(),
    fetchMessages()
  ]);

  return { user, stats, notifications, messages };
}

// ⚠️ Important: If ANY promise rejects, Promise.all() rejects immediately

Promise.race() - First to Complete

Promise.race()
// Promise.race() - resolves/rejects with first completed promise
const slow = new Promise((resolve) => {
  setTimeout(() => resolve('Slow'), 3000);
});

const fast = new Promise((resolve) => {
  setTimeout(() => resolve('Fast'), 1000);
});

Promise.race([slow, fast])
  .then((result) => {
    console.log(result);  // "Fast" (first to complete)
  });

// Practical example: Timeout pattern
function fetchWithTimeout(url, timeout = 5000) {
  const fetchPromise = fetch(url);

  const timeoutPromise = new Promise((_, reject) => {
    setTimeout(() => {
      reject(new Error('Request timeout'));
    }, timeout);
  });

  return Promise.race([fetchPromise, timeoutPromise]);
}

// Usage
async function getData() {
  try {
    const response = await fetchWithTimeout('/api/data', 3000);
    const data = await response.json();
    console.log('Data:', data);
  } catch (error) {
    console.error('Error or timeout:', error.message);
  }
}

Promise.allSettled() - Wait for All (No Rejection)

Promise.allSettled()
// Promise.allSettled() - waits for all, never rejects
const promises = [
  Promise.resolve('Success 1'),
  Promise.reject('Error 1'),
  Promise.resolve('Success 2'),
  Promise.reject('Error 2')
];

Promise.allSettled(promises)
  .then((results) => {
    results.forEach((result) => {
      if (result.status === 'fulfilled') {
        console.log('Success:', result.value);
      } else {
        console.log('Failed:', result.reason);
      }
    });
  });

// Output structure:
// [
//   { status: 'fulfilled', value: 'Success 1' },
//   { status: 'rejected', reason: 'Error 1' },
//   { status: 'fulfilled', value: 'Success 2' },
//   { status: 'rejected', reason: 'Error 2' }
// ]

// Practical example: Multiple independent API calls
async function fetchMultipleUsers(userIds) {
  const promises = userIds.map((id) => fetchUser(id));
  const results = await Promise.allSettled(promises);

  const successful = results
    .filter((r) => r.status === 'fulfilled')
    .map((r) => r.value);

  const failed = results
    .filter((r) => r.status === 'rejected')
    .map((r) => r.reason);

  return { successful, failed };
}

Promise.any() - First to Succeed

Promise.any()
// Promise.any() - resolves with first successful promise
const promise1 = Promise.reject('Error 1');
const promise2 = new Promise((resolve) => setTimeout(() => resolve('Success 2'), 1000));
const promise3 = new Promise((resolve) => setTimeout(() => resolve('Success 3'), 500));

Promise.any([promise1, promise2, promise3])
  .then((result) => {
    console.log(result);  // "Success 3" (first to succeed)
  })
  .catch((error) => {
    console.log('All promises failed:', error);
  });

// Practical example: Fastest mirror
async function fetchFromFastestServer(urls) {
  const promises = urls.map((url) => fetch(url));

  try {
    const response = await Promise.any(promises);
    return await response.json();
  } catch (error) {
    throw new Error('All servers failed');
  }
}

// Usage
const mirrors = [
  'https://api1.example.com/data',
  'https://api2.example.com/data',
  'https://api3.example.com/data'
];

const data = await fetchFromFastestServer(mirrors);

🎯 Practical Examples

Example 1: Sequential vs Parallel Execution

Performance Comparison
// ❌ Sequential (slow - 3 seconds total)
async function fetchSequential() {
  const start = Date.now();

  const user = await fetchUser(1);      // 1 second
  const posts = await fetchPosts(1);    // 1 second
  const comments = await fetchComments(1); // 1 second

  console.log(`Sequential took: ${Date.now() - start}ms`);
  // Output: Sequential took: 3000ms
}

// ✅ Parallel (fast - 1 second total)
async function fetchParallel() {
  const start = Date.now();

  // All start at the same time
  const [user, posts, comments] = await Promise.all([
    fetchUser(1),      // 1 second
    fetchPosts(1),     // 1 second
    fetchComments(1)   // 1 second
  ]);

  console.log(`Parallel took: ${Date.now() - start}ms`);
  // Output: Parallel took: 1000ms (much faster!)
}

// When to use each:
// - Sequential: When next operation depends on previous result
// - Parallel: When operations are independent

Example 2: Retry Logic

Retry Failed Requests
async function fetchWithRetry(url, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      console.log(`Attempt ${i + 1} of ${maxRetries}`);
      const response = await fetch(url);

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
      }

      return await response.json();
    } catch (error) {
      console.log(`Attempt ${i + 1} failed:`, error.message);

      // If last attempt, throw error
      if (i === maxRetries - 1) {
        throw new Error(`Failed after ${maxRetries} attempts`);
      }

      // Wait before retry (exponential backoff)
      await new Promise((resolve) => setTimeout(resolve, 1000 * Math.pow(2, i)));
    }
  }
}

// Usage
try {
  const data = await fetchWithRetry('/api/data', 3);
  console.log('Data:', data);
} catch (error) {
  console.error('All retries failed:', error.message);
}

Example 3: Data Pipeline

Processing Pipeline
// Complete async data pipeline
async function processUserData(userId) {
  try {
    // 1. Fetch user
    console.log('Fetching user...');
    const user = await fetchUser(userId);

    // 2. Fetch related data in parallel
    console.log('Fetching related data...');
    const [posts, friends, settings] = await Promise.all([
      fetchPosts(user.id),
      fetchFriends(user.id),
      fetchSettings(user.id)
    ]);

    // 3. Process data
    console.log('Processing data...');
    const processedPosts = posts.map((post) => ({
      ...post,
      wordCount: post.content.split(' ').length
    }));

    // 4. Calculate statistics
    const stats = {
      totalPosts: posts.length,
      totalFriends: friends.length,
      avgPostLength: processedPosts.reduce((sum, p) => sum + p.wordCount, 0) / posts.length
    };

    // 5. Return complete profile
    return {
      user,
      posts: processedPosts,
      friends,
      settings,
      stats
    };

  } catch (error) {
    console.error('Pipeline error:', error);
    throw error;
  }
}

// Usage
processUserData(1)
  .then((profile) => {
    console.log('Complete profile:', profile);
    displayUserProfile(profile);
  })
  .catch((error) => {
    showErrorMessage(error.message);
  });

Example 4: Loading States

Managing Loading States
// Managing UI loading states with async operations
async function loadUserDashboard(userId) {
  const loadingElement = document.getElementById('loading');
  const contentElement = document.getElementById('content');
  const errorElement = document.getElementById('error');

  try {
    // Show loading
    loadingElement.style.display = 'block';
    contentElement.style.display = 'none';
    errorElement.style.display = 'none';

    // Fetch data
    const [user, stats, notifications] = await Promise.all([
      fetchUser(userId),
      fetchUserStats(userId),
      fetchNotifications(userId)
    ]);

    // Hide loading, show content
    loadingElement.style.display = 'none';
    contentElement.style.display = 'block';

    // Render dashboard
    renderDashboard({ user, stats, notifications });

  } catch (error) {
    // Hide loading, show error
    loadingElement.style.display = 'none';
    errorElement.style.display = 'block';
    errorElement.textContent = `Error: ${error.message}`;

    console.error('Dashboard load error:', error);
  }
}

// Alternative pattern with state object
async function loadData() {
  const state = {
    loading: true,
    data: null,
    error: null
  };

  updateUI(state);

  try {
    state.data = await fetchData();
    state.loading = false;
  } catch (error) {
    state.error = error;
    state.loading = false;
  }

  updateUI(state);
}

✅ Best Practices

💡 Async JavaScript Best Practices
  • Use async/await instead of promise chains for better readability
  • Always use try/catch blocks with async/await
  • Use Promise.all() for parallel operations to improve performance
  • Handle errors at the appropriate level (don't swallow errors)
  • Use Promise.allSettled() when you need all results regardless of failures
  • Implement timeout patterns for network requests
  • Consider retry logic for failed requests
  • Use finally for cleanup operations (hiding loaders, etc.)
  • Avoid mixing callbacks and promises (pick one pattern)
  • Remember: await only works inside async functions

⚠️ Common Mistakes

Async Pitfalls to Avoid
// ❌ Mistake 1: Forgetting await
async function fetchData() {
  const data = fetch('/api/data');  // Missing await!
  console.log(data);  // This is a Promise, not the data!
}

// ✅ Correct
async function fetchData() {
  const response = await fetch('/api/data');
  const data = await response.json();
  console.log(data);  // Now this is the actual data
}

// ❌ Mistake 2: Not handling errors
async function getData() {
  const data = await fetch('/api/data');  // No try/catch!
  return data;
}

// ✅ Correct
async function getData() {
  try {
    const response = await fetch('/api/data');
    return await response.json();
  } catch (error) {
    console.error('Error:', error);
    throw error;
  }
}

// ❌ Mistake 3: Sequential when could be parallel
async function loadPage() {
  const user = await fetchUser();      // Waits
  const posts = await fetchPosts();    // Then waits
  const comments = await fetchComments(); // Then waits
}

// ✅ Correct (if operations are independent)
async function loadPage() {
  const [user, posts, comments] = await Promise.all([
    fetchUser(),
    fetchPosts(),
    fetchComments()
  ]);
}

// ❌ Mistake 4: Using await in loops inefficiently
async function processItems(items) {
  for (const item of items) {
    await processItem(item);  // Processes one at a time
  }
}

// ✅ Correct (if order doesn't matter)
async function processItems(items) {
  await Promise.all(items.map((item) => processItem(item)));
}

// ❌ Mistake 5: Forgetting to return in promise chains
fetchData()
  .then((data) => {
    processData(data);  // Missing return!
  })
  .then((result) => {
    console.log(result);  // undefined!
  });

// ✅ Correct
fetchData()
  .then((data) => {
    return processData(data);  // Return the promise
  })
  .then((result) => {
    console.log(result);  // Now has the result
  });
📝 Summary
  • Asynchronous code allows non-blocking operations
  • Callbacks are the original async pattern (can lead to callback hell)
  • Promises provide cleaner async code with .then() and .catch()
  • async/await makes async code look synchronous and is easier to read
  • try/catch handles errors in async/await
  • Promise.all() runs promises in parallel for better performance
  • Promise.race() returns the first completed promise
  • Promise.allSettled() waits for all promises without rejecting
  • Error handling is crucial in async operations