Module: Asynchronous JS

Promises

JavaScript Essentials: Asynchronous JS - Promises

Asynchronous JavaScript is crucial for building responsive and efficient web applications. Promises are a core part of handling asynchronous operations in modern JavaScript. This document will cover the fundamentals of Promises.

What are Promises?

Promises represent the eventual completion (or failure) of an asynchronous operation and its resulting value. Think of a promise as a placeholder for a value that isn't available yet. They help avoid "callback hell" and make asynchronous code more readable and manageable.

Key Concepts:

  • States: A Promise can be in one of three states:

    • Pending: The initial state; the operation hasn't completed yet.
    • Fulfilled (Resolved): The operation completed successfully, and the promise has a value.
    • Rejected: The operation failed, and the promise has a reason for the failure.
  • resolve and reject: These are functions used to change the state of a Promise.

    • resolve(value): Changes the state to fulfilled with the given value.
    • reject(reason): Changes the state to rejected with the given reason (usually an Error object).

Creating a Promise

You create a Promise using the Promise constructor. The constructor takes a function called the executor as an argument. The executor function receives two arguments: resolve and reject.

const myPromise = new Promise((resolve, reject) => {
  // Asynchronous operation here (e.g., fetching data)
  setTimeout(() => {
    const success = true; // Simulate success or failure

    if (success) {
      resolve("Data fetched successfully!"); // Resolve with a value
    } else {
      reject(new Error("Failed to fetch data.")); // Reject with an error
    }
  }, 2000); // Simulate a 2-second delay
});

Explanation:

  1. new Promise((resolve, reject) => { ... });: Creates a new Promise object.
  2. setTimeout(() => { ... }, 2000);: Simulates an asynchronous operation that takes 2 seconds.
  3. if (success) { resolve("Data fetched successfully!"); }: If the operation is successful, call resolve with the result.
  4. else { reject(new Error("Failed to fetch data.")); }: If the operation fails, call reject with an error object.

Consuming a Promise: .then() and .catch()

Once you have a Promise, you need to consume it to access its eventual value or handle any errors. You do this using the .then() and .catch() methods.

  • .then(onFulfilled, onRejected): This method attaches callbacks to be executed when the Promise is fulfilled or rejected.

    • onFulfilled: A function to be called when the Promise is fulfilled. It receives the resolved value as an argument.
    • onRejected: (Optional) A function to be called when the Promise is rejected. It receives the rejection reason (usually an Error object) as an argument.
  • .catch(onRejected): This method is a shorthand for .then(null, onRejected). It's specifically for handling rejections. It's generally preferred for error handling.

myPromise
  .then(data => {
    console.log("Success:", data); // Output: Success: Data fetched successfully!
    return "Processed data"; // You can return a value to chain another .then()
  })
  .then(processedData => {
    console.log("Processed:", processedData); // Output: Processed: Processed data
  })
  .catch(error => {
    console.error("Error:", error.message); // Output: Error: Failed to fetch data.
  });

Explanation:

  1. myPromise.then(data => { ... });: Attaches a callback to be executed when myPromise is fulfilled. The data variable will contain the value passed to resolve.
  2. myPromise.catch(error => { ... });: Attaches a callback to be executed when myPromise is rejected. The error variable will contain the reason passed to reject.
  3. Chaining: .then() returns a new Promise. This allows you to chain multiple .then() calls together to perform a sequence of asynchronous operations. The return value of one .then() becomes the input to the next.

Promise Chaining

Promise chaining makes asynchronous code more readable and easier to follow. Each .then() call operates on the result of the previous one.

function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("Data from API");
    }, 1000);
  });
}

function processData(data) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("Processed: " + data);
    }, 500);
  });
}

fetchData()
  .then(data => {
    console.log("Fetched:", data);
    return processData(data); // Return the promise from processData
  })
  .then(processedData => {
    console.log("Processed:", processedData);
  })
  .catch(error => {
    console.error("Error:", error);
  });

Promise.all() and Promise.race()

These static methods are useful for working with multiple Promises.

  • Promise.all(iterable): Takes an iterable (e.g., an array) of Promises. It resolves when all of the Promises in the iterable resolve, and the resolved value is an array containing the resolved values of each Promise in the same order as the input iterable. If any Promise rejects, Promise.all() immediately rejects with the reason of the first rejected Promise.

  • Promise.race(iterable): Takes an iterable of Promises. It resolves or rejects as soon as one of the Promises in the iterable resolves or rejects, with the value or reason of that first Promise.

const promise1 = new Promise(resolve => setTimeout(() => resolve("Promise 1"), 500));
const promise2 = new Promise(resolve => setTimeout(() => resolve("Promise 2"), 1000));
const promise3 = new Promise((resolve, reject) => setTimeout(() => reject("Promise 3 failed"), 200));

Promise.all([promise1, promise2, promise3])
  .then(results => console.log("All resolved:", results))
  .catch(error => console.error("All rejected:", error)); // Output: All rejected: Promise 3 failed

Promise.race([promise1, promise2, promise3])
  .then(result => console.log("First resolved:", result))
  .catch(error => console.error("First rejected:", error)); // Output: First rejected: Promise 3 failed

async/await (Syntactic Sugar)

async/await is a more modern and readable way to work with Promises. It's built on top of Promises and makes asynchronous code look and behave a bit more like synchronous code.

  • async: The async keyword is used to define an asynchronous function. An async function always returns a Promise.
  • await: The await keyword can only be used inside an async function. It pauses the execution of the function until the Promise it's awaiting resolves. The await expression returns the resolved value of the Promise.
async function fetchDataAndProcess() {
  try {
    const data = await fetchData();
    console.log("Fetched:", data);
    const processedData = await processData(data);
    console.log("Processed:", processedData);
    return processedData;
  } catch (error) {
    console.error("Error:", error);
  }
}

fetchDataAndProcess();

Explanation:

  1. async function fetchDataAndProcess() { ... }: Defines an asynchronous function.
  2. const data = await fetchData();: Pauses execution until fetchData() resolves, then assigns the resolved value to data.
  3. const processedData = await processData(data);: Pauses execution until processData() resolves, then assigns the resolved value to processedData.
  4. try...catch: Used for error handling. If any Promise rejects within the try block, the catch block will be executed.

Summary

Promises are a powerful tool for managing asynchronous operations in JavaScript. They provide a cleaner and more structured way to handle asynchronous code compared to traditional callbacks. async/await builds on Promises to provide an even more readable and concise syntax. Understanding Promises is essential for building modern, responsive web applications.