Beginner60 min read

Asynchronous JavaScript: Promises and async/await

Master asynchronous JavaScript with Promises and async/await. Essential for API calls, data fetching, and React applications.

Topics Covered:

Promisesasync/awaitFetch APIError HandlingAsync Patterns

Prerequisites:

  • JavaScript Arrays and Objects

Video Tutorial

Overview

Asynchronous JavaScript allows your code to run without blocking. This is essential for fetching data from APIs, which is a core part of React applications. This tutorial covers Promises, async/await, the Fetch API, error handling, and common async patterns. Understanding asynchronous JavaScript is crucial for building real-world React applications that interact with APIs and handle data loading.

Understanding Asynchronous Code

JavaScript is single-threaded but can handle asynchronous operations. Understanding how this works is crucial. Synchronous vs Asynchronous: • Synchronous: Code runs in order, blocks execution • Asynchronous: Code runs later, doesn't block Why Asynchronous: • Network requests take time • File operations take time • Don't want to freeze UI • Better user experience Common Async Operations: • Fetching data from APIs • Reading files • Timers (setTimeout, setInterval) • Database operations

Code Example:
// Synchronous code (blocks)
console.log("1");
console.log("2");
console.log("3");
// Output: 1, 2, 3 (in order)

// Asynchronous code (doesn't block)
console.log("1");
setTimeout(() => {
  console.log("2");
}, 1000);
console.log("3");
// Output: 1, 3, 2 (2 comes later)

// Real-world example: Fetching data
console.log("Loading...");
fetch("/api/users")
  .then(response => response.json())
  .then(data => {
    console.log("Data loaded:", data);
  });
console.log("This runs immediately, before data loads");

Asynchronous code doesn't block execution. Operations like API calls happen in the background, allowing other code to run. This is essential for responsive applications.

Promises

Promises represent the eventual completion (or failure) of an asynchronous operation. Promise States: • Pending - Initial state • Fulfilled - Operation succeeded • Rejected - Operation failed Promise Methods: • then() - Handle success • catch() - Handle errors • finally() - Run regardless of outcome Creating Promises: • new Promise() constructor • Resolve on success • Reject on failure

Code Example:
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
  const success = true;
  
  if (success) {
    resolve("Operation succeeded!");
  } else {
    reject("Operation failed!");
  }
});

// Using Promises
myPromise
  .then(result => {
    console.log(result); // "Operation succeeded!"
  })
  .catch(error => {
    console.error(error); // "Operation failed!"
  })
  .finally(() => {
    console.log("Promise completed");
  });

// Chaining Promises
fetch("/api/users")
  .then(response => response.json())
  .then(users => {
    console.log(users);
    return fetch(`/api/users/${users[0].id}`);
  })
  .then(response => response.json())
  .then(user => {
    console.log(user);
  })
  .catch(error => {
    console.error("Error:", error);
  });

// Promise.all - Wait for all promises
const promise1 = fetch("/api/users");
const promise2 = fetch("/api/posts");
const promise3 = fetch("/api/comments");

Promise.all([promise1, promise2, promise3])
  .then(responses => {
    // All promises resolved
    return Promise.all(responses.map(r => r.json()));
  })
  .then(([users, posts, comments]) => {
    console.log({ users, posts, comments });
  });

Promises handle asynchronous operations. Use then() for success, catch() for errors. Chain promises for sequential operations. Promise.all() waits for multiple promises.

async/await

async/await provides a cleaner syntax for working with Promises. It makes asynchronous code look like synchronous code. async Functions: • Function declared with async • Always returns a Promise • Can use await inside await Keyword: • Waits for Promise to resolve • Can only be used in async functions • Makes code easier to read Error Handling: • Use try/catch blocks • Catch handles rejected promises Why Use async/await: • Cleaner syntax • Easier to read • Better error handling • Preferred in modern JavaScript

Code Example:
// async function
async function fetchUser() {
  try {
    const response = await fetch("/api/user");
    const user = await response.json();
    return user;
  } catch (error) {
    console.error("Error fetching user:", error);
    throw error;
  }
}

// Using async function
fetchUser()
  .then(user => console.log(user))
  .catch(error => console.error(error));

// Multiple await calls
async function fetchUserData(userId) {
  try {
    const user = await fetch(`/api/users/${userId}`).then(r => r.json());
    const posts = await fetch(`/api/users/${userId}/posts`).then(r => r.json());
    const comments = await fetch(`/api/users/${userId}/comments`).then(r => r.json());
    
    return { user, posts, comments };
  } catch (error) {
    console.error("Error:", error);
  }
}

// Parallel async operations
async function fetchAllData(userId) {
  try {
    const [user, posts, comments] = await Promise.all([
      fetch(`/api/users/${userId}`).then(r => r.json()),
      fetch(`/api/users/${userId}/posts`).then(r => r.json()),
      fetch(`/api/users/${userId}/comments`).then(r => r.json())
    ]);
    
    return { user, posts, comments };
  } catch (error) {
    console.error("Error:", error);
  }
}

// async/await in React (preview)
async function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    async function loadUser() {
      try {
        const response = await fetch(`/api/users/${userId}`);
        const userData = await response.json();
        setUser(userData);
      } catch (error) {
        console.error("Error:", error);
      } finally {
        setLoading(false);
      }
    }
    
    loadUser();
  }, [userId]);
  
  if (loading) return <div>Loading...</div>;
  return <div>{user.name}</div>;
}

async/await makes asynchronous code easier to read and write. Use try/catch for error handling. await waits for Promises to resolve. Essential for React data fetching.

Fetch API

The Fetch API is the modern way to make HTTP requests in JavaScript. It's used extensively in React applications. Fetch Basics: • fetch(url) returns a Promise • Response object contains data • Use .json() to parse JSON • Handle errors properly Fetch Options: • method - HTTP method (GET, POST, etc.) • headers - Request headers • body - Request body • credentials - Include cookies Error Handling: • fetch doesn't reject on HTTP errors • Check response.ok • Handle network errors

Code Example:
// Basic GET request
fetch("/api/users")
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error("Error:", error));

// With async/await
async function getUsers() {
  try {
    const response = await fetch("/api/users");
    const users = await response.json();
    return users;
  } catch (error) {
    console.error("Error:", error);
  }
}

// POST request
async function createUser(userData) {
  try {
    const response = await fetch("/api/users", {
      method: "POST",
      headers: {
        "Content-Type": "application/json"
      },
      body: JSON.stringify(userData)
    });
    
    if (!response.ok) {
      throw new Error("Failed to create user");
    }
    
    const newUser = await response.json();
    return newUser;
  } catch (error) {
    console.error("Error:", error);
  }
}

// Error handling
async function fetchData(url) {
  try {
    const response = await fetch(url);
    
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    
    const data = await response.json();
    return data;
  } catch (error) {
    console.error("Fetch error:", error);
    throw error;
  }
}

// Fetch in React (preview)
function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    async function loadUsers() {
      try {
        const response = await fetch("/api/users");
        if (!response.ok) throw new Error("Failed to fetch");
        const data = await response.json();
        setUsers(data);
      } catch (error) {
        console.error("Error:", error);
      } finally {
        setLoading(false);
      }
    }
    
    loadUsers();
  }, []);
  
  if (loading) return <div>Loading...</div>;
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

Fetch API makes HTTP requests. Returns a Promise. Use async/await for cleaner code. Always check response.ok and handle errors. Essential for React data fetching.

Conclusion

Asynchronous JavaScript is essential for React applications. You've learned about Promises, async/await, and the Fetch API. These concepts are used constantly in React for fetching data, handling user interactions, and managing side effects. Practice making API calls and handling errors. Understanding async JavaScript is crucial before diving into React hooks like useEffect, which handle asynchronous operations. Next, you'll be ready to start learning React!