Async/Await in JavaScript - Complete Guide

Async/await is a modern way to handle asynchronous operations in JavaScript. It makes asynchronous code look and behave more like synchronous code, making it easier to read and maintain.

Understanding Asynchronous JavaScript #

JavaScript is single-threaded, meaning it can only execute one operation at a time. Asynchronous operations allow JavaScript to perform tasks like API calls, file operations, or timers without blocking the main thread.

The Evolution of Async JavaScript #

Callbacks (The Old Way) #

function getData(callback) {
  setTimeout(() => {
    callback('Data received');
  }, 1000);
}

getData((data) => {
  console.log(data);
});

Problem: Callback hell - nested callbacks become difficult to read and maintain.

Promises (Better) #

function getData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('Data received');
    }, 1000);
  });
}

getData()
  .then(data => console.log(data))
  .catch(error => console.error(error));

Async/Await (Best) #

async function getData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('Data received');
    }, 1000);
  });
}

async function main() {
  const data = await getData();
  console.log(data);
}

main();

Basic Async/Await Syntax #

The async Keyword #

Adding async before a function makes it return a Promise automatically:

async function sayHello() {
  return 'Hello!';
}

// This is equivalent to:
function sayHello() {
  return Promise.resolve('Hello!');
}

// Usage:
sayHello().then(message => console.log(message)); // "Hello!"

The await Keyword #

await pauses the execution of an async function until the Promise resolves:

async function fetchUser() {
  const response = await fetch('https://api.example.com/user/1');
  const user = await response.json();
  return user;
}

Important: You can only use await inside an async function.

Practical Examples #

Making API Calls #

async function fetchUserData(userId) {
  try {
    const response = await fetch(`https://api.example.com/users/${userId}`);

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

    const user = await response.json();
    return user;
  } catch (error) {
    console.error('Error fetching user:', error);
    throw error;
  }
}

// Usage
async function main() {
  const user = await fetchUserData(123);
  console.log(user);
}

main();

Sequential vs Parallel Execution #

Sequential (Slower):

async function getDataSequential() {
  const user = await fetch('https://api.example.com/user/1').then(r => r.json());
  const posts = await fetch('https://api.example.com/posts/1').then(r => r.json());
  const comments = await fetch('https://api.example.com/comments/1').then(r => r.json());

  return { user, posts, comments };
}

// Total time: Time1 + Time2 + Time3

Parallel (Faster):

async function getDataParallel() {
  const [user, posts, comments] = await Promise.all([
    fetch('https://api.example.com/user/1').then(r => r.json()),
    fetch('https://api.example.com/posts/1').then(r => r.json()),
    fetch('https://api.example.com/comments/1').then(r => r.json())
  ]);

  return { user, posts, comments };
}

// Total time: Max(Time1, Time2, Time3)

Error Handling with Try/Catch #

async function fetchWithErrorHandling() {
  try {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Failed to fetch data:', error);
    return null;
  }
}

Multiple Try/Catch Blocks #

async function complexOperation() {
  let user;
  let posts;

  try {
    user = await fetchUser();
  } catch (error) {
    console.error('Failed to fetch user:', error);
    user = { name: 'Guest' }; // Fallback
  }

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

  return { user, posts };
}

Advanced Patterns #

Async IIFE (Immediately Invoked Function Expression) #

(async () => {
  const data = await fetchData();
  console.log(data);
})();

Async Array Methods #

Using for…of (Correct):

async function processUsers(userIds) {
  const results = [];

  for (const id of userIds) {
    const user = await fetchUser(id);
    results.push(user);
  }

  return results;
}

Using map with Promise.all (Parallel):

async function processUsersParallel(userIds) {
  const promises = userIds.map(id => fetchUser(id));
  const results = await Promise.all(promises);
  return results;
}

Warning: Regular forEach won’t work with await:

// This DOESN'T work as expected
userIds.forEach(async (id) => {
  const user = await fetchUser(id); // Runs immediately, doesn't wait
  console.log(user);
});

Promise.all vs Promise.allSettled #

Promise.all - Fails if any promise rejects:

async function fetchAll() {
  try {
    const [user, posts, comments] = await Promise.all([
      fetchUser(),
      fetchPosts(),
      fetchComments()
    ]);
    return { user, posts, comments };
  } catch (error) {
    // If any request fails, we end up here
    console.error('One of the requests failed:', error);
  }
}

Promise.allSettled - Waits for all promises, regardless of success/failure:

async function fetchAllSettled() {
  const results = await Promise.allSettled([
    fetchUser(),
    fetchPosts(),
    fetchComments()
  ]);

  results.forEach((result, index) => {
    if (result.status === 'fulfilled') {
      console.log(`Request ${index} succeeded:`, result.value);
    } else {
      console.log(`Request ${index} failed:`, result.reason);
    }
  });

  return results;
}

Async Functions with Timeout #

function timeout(ms) {
  return new Promise((_, reject) => {
    setTimeout(() => reject(new Error('Timeout')), ms);
  });
}

async function fetchWithTimeout(url, ms = 5000) {
  try {
    const response = await Promise.race([
      fetch(url),
      timeout(ms)
    ]);
    return await response.json();
  } catch (error) {
    if (error.message === 'Timeout') {
      console.error('Request timed out');
    }
    throw error;
  }
}

Retry Logic #

async function fetchWithRetry(url, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      const response = await fetch(url);
      return await response.json();
    } catch (error) {
      console.log(`Attempt ${i + 1} failed`);

      if (i === maxRetries - 1) {
        throw error; // Final attempt failed
      }

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

Common Mistakes and How to Avoid Them #

Mistake 1: Forgetting await #

// Wrong - Returns a Promise, not the data
async function getData() {
  const data = fetch('https://api.example.com/data');
  return data; // Promise<Response>
}

// Correct
async function getData() {
  const response = await fetch('https://api.example.com/data');
  const data = await response.json();
  return data;
}

Mistake 2: Not Handling Errors #

// Risky - Errors will crash the application
async function fetchData() {
  const data = await fetch('https://api.example.com/data');
  return data;
}

// Safe
async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data');
    return await response.json();
  } catch (error) {
    console.error('Fetch failed:', error);
    return null;
  }
}

Mistake 3: Sequential When You Want Parallel #

// Slow - Takes 2 seconds
async function slow() {
  const a = await delay(1000);
  const b = await delay(1000);
  return [a, b];
}

// Fast - Takes 1 second
async function fast() {
  const [a, b] = await Promise.all([
    delay(1000),
    delay(1000)
  ]);
  return [a, b];
}

Best Practices #

  1. Always handle errors with try/catch or .catch()
  2. Use async/await over .then() for cleaner code
  3. Run independent operations in parallel with Promise.all
  4. Don’t use await in loops unless you need sequential execution
  5. Return early from async functions when possible
  6. Add timeouts for network requests
  7. Use async IIFE for top-level await (before ES2022)

Browser and Node.js Support #

Async/await is supported in:

  • Node.js 7.6+
  • Chrome 55+
  • Firefox 52+
  • Safari 10.1+
  • Edge 15+

For older browsers, use transpilers like Babel.

Async/await makes asynchronous JavaScript significantly more readable and maintainable. Master these patterns to write better JavaScript code.