Topic 002: Closure, Callback, Async/Await, Promises
Sure! Let’s go through each concept with some JavaScript code examples.
Closure is a fundamental concept in JavaScript that allows functions to retain access to variables from their containing scope even after the parent function has finished executing. This means that inner functions have access to the variables and parameters of their outer function, even after the outer function has returned.
function outerFunction() {
  let outerVar = "I am outer";
  function innerFunction() {
    console.log(outerVar);
  }
  return innerFunction;
}
let closureExample = outerFunction();
closureExample(); // Output: I am outer
In this example, innerFunction retains access to the outerVar variable even after outerFunction has finished executing. This is possible because of closure.
Callbacks are functions that are passed as arguments to another function and are executed after the completion of that function. They are commonly used in asynchronous programming to handle asynchronous tasks.
function fetchData(callback) {
  setTimeout(() => {
    callback("Data fetched");
  }, 2000);
}
function displayData(data) {
  console.log(data);
}
fetchData(displayData); // Output (after 2 seconds): Data fetched
In this example, fetchData is an asynchronous function that simulates fetching data after 2 seconds. The displayData function is passed as a callback to fetchData, and it gets executed with the fetched data after the asynchronous task completes.
Promises are objects representing the eventual completion or failure of an asynchronous operation. They allow you to handle asynchronous operations more elegantly than callbacks.
function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("Data fetched");
    }, 2000);
  });
}
fetchData()
  .then((data) => console.log(data)) // Output (after 2 seconds): Data fetched
  .catch((error) => console.error(error));
In this example, fetchData returns a Promise that resolves with the fetched data after 2 seconds. We use the then method to handle the successful resolution of the Promise and the catch method to handle any errors that might occur.
A Promise in JavaScript represents a future value or an asynchronous operation that will eventually produce a value or result. It can be in one of the three states: pending, fulfilled, or rejected.
Here’s an example of a Promise in JavaScript:
// Creating a promise
const myPromise = new Promise((resolve, reject) => {
  // Asynchronous operation, e.g., fetching data
  setTimeout(() => {
    const data = 42;
    if (data) {
      resolve(data); // If successful, call resolve with the result
    } else {
      reject(new Error("Data not found")); // If failed, call reject with an error
    }
  }, 2000); // Simulating a delay of 2 seconds
});
// Consuming the promise
myPromise
  .then((result) => {
    console.log("Promise resolved with result:", result);
  })
  .catch((error) => {
    console.error("Promise rejected with error:", error);
  });
In this code:
new Promise() constructor. Inside the constructor, we define an executor function that takes two parameters: resolve and reject. These are functions provided by JavaScript to either fulfill or reject the promise.setTimeout here). When the operation completes successfully, we call resolve with the result. If the operation fails, we call reject with an error..then() method, which takes a callback function to handle the resolved value, and the .catch() method, which takes a callback function to handle any errors that occur during the promise execution.We can also create custom promises for our specific needs. Here’s an example:
function customPromise(condition) {
  return new Promise((resolve, reject) => {
    if (condition) {
      resolve("Custom promise resolved");
    } else {
      reject(new Error("Custom promise rejected"));
    }
  });
}
// Consuming the custom promise
customPromise(true)
  .then((result) => {
    console.log(result);
  })
  .catch((error) => {
    console.error(error);
  });
In this code:
customPromise that takes a condition parameter.condition, we either resolve the promise with a success message or reject it with an error message..then() and .catch() to handle the resolution or rejection.Promise.all() to make concurrent API calls and handle their responses once they’re all complete. Here’s a basic example:const axios = require("axios");
async function fetchData() {
  try {
    const [response1, response2, response3] = await Promise.all([
      axios.get("https://api.endpoint1.com"),
      axios.get("https://api.endpoint2.com"),
      axios.get("https://api.endpoint3.com"),
    ]);
    // Handle responses here
    console.log(response1.data);
    console.log(response2.data);
    console.log(response3.data);
  } catch (error) {
    console.error("Error fetching data:", error);
  }
}
fetchData();
This code will make three API calls concurrently using Axios (you can use any HTTP client library), and once all three calls are complete, it will handle the responses accordingly.
Async/await is a syntactic sugar built on top of Promises, providing a more readable and synchronous-like way to write asynchronous code.
async function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("Data fetched");
    }, 2000);
  });
}
async function displayData() {
  try {
    const data = await fetchData();
    console.log(data); // Output (after 2 seconds): Data fetched
  } catch (error) {
    console.error(error);
  }
}
displayData();
In this example, fetchData returns a Promise as before. However, with async/await, we can use the await keyword to pause the execution of displayData until the Promise returned by fetchData is resolved or rejected. This results in code that looks more synchronous and is easier to understand.