Learnwizy Technologies Logo

Learnwizy Technologies

Class 13: Asynchronous Programming (Promises and async/await)

In the previous class, we introduced asynchronous programming with setTimeout and setInterval to handle tasks without blocking the main thread. Today, we delve into more advanced and powerful patterns for managing asynchronous operations: Promises and the modern Async/Await syntax, which are essential for handling network requests and other time-consuming operations in a clean and efficient manner.


Understanding Callback Hell and the Need for Better Patterns

Before Promises, deeply nested callbacks were common for handling sequential asynchronous operations, leading to what's known as "callback hell" or "pyramid of doom." This code becomes hard to read, maintain, and debug.

// Example of Callback Hell (illustrative - not runnable)
fetchUser(function(user) {
  fetchUserPosts(user.id, function(posts) {
    fetchPostComments(posts[0].id, function(comments) {
      console.log("User, posts, and comments fetched:", { user, posts, comments });
    }, function(err) {
      console.error("Error fetching comments:", err);
    });
  }, function(err) {
    console.error("Error fetching posts:", err);
  });
}, function(err) {
  console.error("Error fetching user:", err);
});

Promises were introduced to provide a more structured and readable way to handle asynchronous operations, especially when they are chained together.


Promises

A Promise is an object representing the eventual completion or failure of an asynchronous operation. A Promise can be in one of three states:

// Creating a simple Promise
const myPromise = new Promise((resolve, reject) => {
  // Simulate an asynchronous operation (e.g., fetching data)
  const success = true; // Change to false to see the 'catch' block

  setTimeout(() => {
    if (success) {
      resolve("Data fetched successfully!"); // Operation succeeded
    } else {
      reject("Failed to fetch data.");     // Operation failed
    }
  }, 1000);
});

// Consuming the Promise
myPromise.then((message) => {
  console.log("Success:", message); // Runs if the promise is fulfilled
}).catch((error) => {
  console.error("Error:", error);   // Runs if the promise is rejected
}).finally(() => {
  console.log("Promise operation finished."); // Runs regardless of success or failure
});

console.log("Promise initiated..."); // This runs first

/* Expected Output (if success is true):
Promise initiated...
(after 1 second)
Success: Data fetched successfully!
Promise operation finished.
*/

/* Expected Output (if success is false):
Promise initiated...
(after 1 second)
Error: Failed to fetch data.
Promise operation finished.
*/

Chaining Promises (.then())

Promises can be chained to perform a sequence of asynchronous operations. Each .then() block returns a new Promise, allowing you to chain the next operation.

function fetchData(message, delay) {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log(message);
      resolve(message); // Resolve with the message for the next chain
    }, delay);
  });
}

fetchData("Step 1: Fetching user data...", 1000)
  .then(data => fetchData("Step 2: Processing user data...", 800))
  .then(data => fetchData("Step 3: Fetching user posts...", 1200))
  .then(data => console.log("All steps completed."))
  .catch(error => console.error("An error occurred in the chain:", error));

/* Expected Output (with delays):
Step 1: Fetching user data...
Step 2: Processing user data...
Step 3: Fetching user posts...
All steps completed.
*/

Using the fetch API for HTTP Requests

The fetch() API provides a modern, Promise-based interface for making network requests (e.g., to fetch data from a server, submit form data). It replaces the older XMLHttpRequest.

Basic GET Request with fetch

// Fetch data from a public API (e.g., JSONPlaceholder)
fetch("https://jsonplaceholder.typicode.com/posts/1")
  .then(response => {
    // Check if the request was successful (status code 2xx)
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    return response.json(); // Parse the JSON response body
  })
  .then(data => {
    console.log("Fetched post:", data);
    console.log("Title:", data.title);
  })
  .catch(error => {
    console.error("Error fetching data:", error);
  });

console.log("Request sent..."); // This runs first

/* Expected Output:
Request sent...
(after network delay)
Fetched post: { userId: 1, id: 1, title: 'sunt aut...', body: 'quia et ...' }
Title: sunt aut facere repellat provident occaecati excepturi optio reprehenderit
*/

POST Request with fetch

const newPost = {
  title: 'foo',
  body: 'bar',
  userId: 1,
};

fetch("https://jsonplaceholder.typicode.com/posts", {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(newPost), // Convert JavaScript object to JSON string
})
  .then(response => {
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    return response.json();
  })
  .then(data => {
    console.log("New post created:", data);
  })
  .catch(error => {
    console.error("Error creating post:", error);
  });

Simplifying Async Code with Async/Await

async/await is a modern JavaScript syntax (introduced in ES2017) built on top of Promises that makes asynchronous code look and behave more like synchronous code, making it much easier to read and write.

async function getPostTitle(postId) {
  try {
    // await pauses execution until the fetch Promise resolves
    const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`);

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

    // await pauses execution until the .json() Promise resolves
    const data = await response.json();

    console.log(`Post ${postId} title:`, data.title);
    return data.title;
  } catch (error) {
    console.error("Failed to get post title:", error);
    return null;
  }
}

console.log("Calling getPostTitle...");
getPostTitle(2);
getPostTitle(9999); // Will likely cause an error (e.g., 404 Not Found)
console.log("Function call initiated...");

/* Expected Output:
Calling getPostTitle...
Function call initiated...
(after network delay for post 2)
Post 2 title: qui est esse inventore
(after network delay for post 9999)
Failed to get post title: Error: HTTP error! status: 404
*/