Promises in JS : made simple

Problems which leads to promises concept ?

Callback hell (Pyramid of doom) :

A situation where multiple callback functions are nested in order to handle asynchronous operations. which leads to problem like :

  1. Code that is hard to read because of nesting.

  2. Difficult debugging and maintenance due to structure.

  3. Reduced scalibility as adding or modifying functionality becomes complex.

  4.  getUser(userId, (user) => {
       getOrders(user, (orders) => {
         getOrderDetails(orders[0], (details) => {
           processOrder(details, (result) => {
             console.log('Order processed:', result);
           });
         });
       });
     });
    

Error handling issues :

It is difficult to handle error handling with callbacks as there we need to explicitly handle errors, leading to scattered and inconsistent error management.

asyncOperation((err, data) => {
  if (err) {
    console.error(err);
    return;
  }
  nextOperation(data, (err, result) => {
    if (err) {
      console.error(err);
      return;
    }
    console.log(result);
  });
});

Difficult to understand control flow :

With callbacks, it’s difficult to understand clearly who controls the execution and how the call back is called.

function fetchData(callback) {
  setTimeout(() => {
    callback(null, 'data'); // Callback executed here
  }, 1000);
}
fetchData((err, data) => {
  if (err) return console.error(err);
  console.log(data);
});

in above example it’s difficult to undestand the flow clearly, though maybe you think its understandable but when the code base gets large then it gets difficult more and can lead to unwanted consequences.

Scattered state management :

Using callbacks, we often have to pass the state mannually between functions, making the code base cumbersome and error-prone. because, most probably know what a simple spelling mistake or one left “.” can do to your whole application functionality.

function fetchData(callback) {
  setTimeout(() => callback(null, { userId: 1, name: "Rishab" }), 1000);
}

function fetchPosts(userId, callback) {
  setTimeout(() => callback(null, [{ id: 1, content: "Post 1" }]), 1000);
}

function fetchComments(postId, callback) {
  setTimeout(() => callback(null, [{ id: 1, comment: "Great post!" }]), 1000);
}

// State passed explicitly through callbacks
fetchData((err, user) => {
  if (err) return console.error(err);
  fetchPosts(user.userId, (err, posts) => {
    if (err) return console.error(err);
    fetchComments(posts[0].id, (err, comments) => {
      if (err) return console.error(err);
      console.log("User:", user);
      console.log("Posts:", posts);
      console.log("Comments:", comments);
    });
  });
});

What is Promises ?

A Promise in JavaScript is an object that represents the eventual completion (or failure) of an asynchronous operation and its resulting value. It is a way to handle asynchronous code more effectively, avoiding issues like callback hell. or, in simple words , It is just a sugar coated syntax which makes the code base and execution flow more clear.

Usage :

It is used to handle asyncronous operations such as :

  1. fetching the data from the server.

  2. reading a big file.

  3. executing a time or wait for some operations to complete.

     function fetchAPI(url) {
       return new Promise((resolve, reject) => {
         setTimeout(() => {
           if (url) {
             resolve(`Data from ${url}`);
           } else {
             reject("Invalid URL");
           }
         }, Math.random() * 2000);
       });
     }
    
     const api1 = fetchAPI("https://api.example.com/users");
     const api2 = fetchAPI("https://api.example.com/posts");
     const api3 = fetchAPI("https://api.example.com/comments");
    
     Promise.all([api1, api2, api3])
       .then((responses) => {
         console.log("All API responses:", responses);
         // Output: ["Data from https://api.example.com/users", "Data from https://api.example.com/posts", "Data from https://api.example.com/comments"]
       })
       .catch((error) => {
         console.error("Error:", error);
       });
    

Basics of Promises :

  1. States :

    The Promise constructor takes a function with two parameters: resolve and reject.

    1. Pending : a state before entering the execution phase, wait for it’s turn.

    2. Fulfilled (Resolved) : the operation completed successfully, and the promise has some output.

    3. Rejected : there was an error during the operation, and the promise has a reason for the failure.

      
       //1.
       const myPromise = new Promise((resolve, reject) => {
         // Asynchronous operation goes here
         // If successful, call resolve with the result
         // If there's an error, call reject with the error
       });
      
       //2.
       function fetchData() {
         return new Promise((resolve, reject) => {
           setTimeout(() => {
             resolve("Data fetched");
             resolve("Oops, called again!"); // This will be ignored
           }, 1000);
         });
       }
       fetchData()
         .then((data) => {
           console.log("Success:", data);
         })
         .catch((err) => {
           console.error("Error:", err);
         });
       // Output:
       // Success: Data fetched
      
  2. Promise Chaining : Promises support chaining through the then method, allowing you to sequence asynchronous operations in a readable manner.

    • Promise.all() : a method provided by JavaScript to handle multiple Promises concurrently. It takes an array of Promises and returns a single Promise that resolves when all the Promises in the array are fulfilled or rejects as soon as one Promise fails.
  1.  asyncOperation1()
       .then(asyncOperation2)
       .then(asyncOperation3)
       .then((data3) => console.log(data3))
       .catch((err) => console.error(err));
    
     // Or run operations in parallel:
     Promise.all([asyncOperation1(), asyncOperation2()])
       .then((results) => console.log(results))
       .catch((err) => console.error(err));
    
  1. Error handling : Promises have built-in, global error catcher which means multiple functions one error catching, through the catch method, making it easier to manage and propagate errors in asynchronous code.

  2.  function fetchUser() {
       return new Promise((resolve, reject) => {
         setTimeout(() => reject("User not found"), 1000); // Simulate rejection
       });
     }
    
     function fetchPosts() {
       return new Promise((resolve) => {
         setTimeout(() => resolve(["Post 1", "Post 2"]), 1000);
       });
     }
    
     fetchUser()
       .then((user) => {
         console.log("User:", user);
         return fetchPosts()})
       .then((posts) => {console.log("Posts:", posts)})
       .then(()=>{console.log("Posts are successfully fetched from the user.")})
       .catch((error) => {
         console.error("Error:", error); // Output: Error: User not found
       });
    

Solutions of problems which leads to promises concept with Promises :

  1. Callback hell :

     fetchData()
       .then(processData)
       .then(saveData)
       .then((response) => console.log('Data saved:', response))
       .catch((error) => console.error(error));
    
  2. Error handling :

     asyncOperation()
       .then(nextOperation)
       .then((result) => console.log(result))
       .catch((err) => console.error(err));
    
  3. Difficult to understand control flow :

     function fetchData() {
       return new Promise((resolve, reject) => {
         setTimeout(() => resolve('data'), 1000);
       });
     }
     fetchData()
       .then((data) => console.log(data))
       .catch((err) => console.error(err));
    
  4. Scattered state management :

     function fetchData() {
       return new Promise((resolve) => {
         setTimeout(() => resolve({ userId: 1, name: "Rishab" }), 1000);
       });
     }
    
     function fetchPosts(userId) {
       return new Promise((resolve) => {
         setTimeout(() => resolve([{ id: 1, content: "Post 1" }]), 1000);
       });
     }
    
     function fetchComments(postId) {
       return new Promise((resolve) => {
         setTimeout(() => resolve([{ id: 1, comment: "Great post!" }]), 1000);
       });
     }
    
     // State is implicitly managed in the chain
     fetchData()
       .then((user) => {
         console.log("User:", user);
         return fetchPosts(user.userId);
       })
       .then((posts) => {
         console.log("Posts:", posts);
         return fetchComments(posts[0].id);
       })
       .then((comments) => {
         console.log("Comments:", comments);
       })
       .catch((err) => console.error(err));
    

The best way to understand the promises and asynchronous programming is to think them with the real world examples like :

  1. You have a weather app that:

    1. Fetches the user's current location asynchronously.

    2. Uses the location to fetch weather data from a third-party API.

    3. Displays the results once both tasks are completed.

       function getCurrentLocation() {
         return new Promise((resolve, reject) => {
           // Simulating an asynchronous geolocation API
           setTimeout(() => {
             const success = true; // Simulate success or failure
             if (success) {
               console.log("Fetched user location");
               resolve({ latitude: 28.6139, longitude: 77.2090 }); // Example: Delhi
             } else {
               reject("Failed to get location");
             }
           }, 1000);
         });
       }
      
       function getWeatherData(location) {
         return new Promise((resolve, reject) => {
           // Simulating an asynchronous weather API call
           setTimeout(() => {
             console.log("Fetched weather data for location");
             resolve({ temperature: 25, condition: "Sunny", location });
           }, 1000);
         });
       }
      
       getCurrentLocation()
         .then((location) => {
           console.log("Location:", location);
           return getWeatherData(location);
         })
         .then((weather) => {
           console.log("Weather Data:", weather);
         })
         .catch((error) => {
           console.error("Error:", error);
         });