Understanding Promises in JavaScript

Understanding Promises in JavaScript

Promises in JavaScript represent the eventual completion (or failure) of an asynchronous operation and its resulting value. Promises are a powerful way to handle asynchronous tasks, such as network requests or file operations, and they help to avoid callback hell and improve the readability of the code.

Promise States

A promise can be in one of three states:

  1. Pending: The initial state, neither fulfilled nor rejected.

  2. Fulfilled: The operation was completed successfully.

  3. Rejected: The operation failed.

Creating and Consuming Promises

Let's start by creating a promise. We use the Promise constructor, which takes a callback function with two parameters: resolve and reject. These are methods used to indicate the completion or failure of the promise.

const promiseOne = new Promise(function(resolve, reject){
  // Simulating an asynchronous task
  setTimeout(function(){
    console.log("Async task is completed");
    resolve(); // without this, promise will not be consumed
  }, 1000);
});

// Consuming the promise
promiseOne.then(function(){
  console.log("Promise consumed");
});

//outputs:
//Async task is completed
//promise consumed

In the above code, the promise is created and then consumed using the .then() method. The .then() method takes a callback function that is executed when the promise is resolved. Initially, if we forget to call resolve() in our promise, the then() method will not be triggered.

We can also write above function as:

new Promise(function(resolve, reject){
  // Simulating an asynchronous task
  setTimeout(function(){
    console.log("Async task 2");
    resolve(); // without this, promise will not be consumed
  }, 1000);
}).then(function(){
    console.log("Async task 2 is resolved")
})

//outputs:
//Async task 2
//Async task 2 is resolved

Passing Data with Promises

Often, promises are used to handle data fetched from a network or other asynchronous operations. Data can be passed to the resolve() method as an argument, which can then be accessed in the .then() method.

const promiseTwo = new Promise(function(resolve, reject){
  setTimeout(function(){
    resolve({username: "javascript", email: "js@example.com"});
  }, 1000);
});

promiseTwo.then(function(data){
  console.log(data); // {username: "javascript", email: "js@example.com"}
});
  • This work of passing values is done by resolve()

  • We can pass data as parameters to resolve()

  • Mostly data is passed in the form of object, but we can also pass arrays, functions, etc.

Handling Errors with Promises

Promises also allow us to handle errors using the .catch() method. If the promise is rejected, the callback function in the .catch() method is executed.

const promiseThree = new Promise(function(resolve, reject){
  setTimeout(function(){
    let error = true;
    if (!error) {
      resolve({username: "js with harsh", password: "123"});
    } else {
      reject("Error: Something went wrong");
    }
  }, 1000);
});

 promiseThree.then(function(data_received){
     console.log(data_received);
 }).catch(function(rejection_received){
     console.log(rejection_received);
 })

//outputs: Error: Something went wrong

Returning Values from Promises

When handling promises, you can return values from the .then() method, which can be accessed by chaining additional .then() calls.

const promiseFour = new Promise(function(resolve, reject){
  setTimeout(function(){
    let error = false;
    if (!error) {
      resolve({username: "js with harsh", password: "123"});
    } else {
      reject("Error: Something went wrong");
    }
  }, 1000);
});

promiseFour
  .then((user) => {
    console.log(user);
    return user.username;
  })
  .then((myUsername) => {
    console.log(myUsername);
  })
  .catch((error) => {
    console.log(error);
  })
  .finally(() => {
    console.log("The promise is either resolved or rejected");
  });

//outputs:
//Error: Something went wrong
//The promise is either resolved or rejected

In this example, myUsername is the value returned from the first .then() callback, and it is passed to the next .then() callback.

The .finally() method is executed regardless of the promise's outcome, making it a good place to clean up resources or perform other final actions.

Using Async/Await with Promises

Async/Await is a more recent syntax for handling promises, making the code look synchronous and easier to read.

const promiseFive = new Promise(function(resolve, reject){
  setTimeout(function(){
    let error = false;
    if (!error) {
      resolve({username: "javascript", password: "123"});
    } else {
      reject("ERROR: JS went wrong");
    }
  }, 1000);
});

async function consumePromiseFive() {
    const response = await promiseFive;
    console.log(response);
  } 

consumePromiseFive();

//outputs: {username: 'javascript', password: '123'}

What if "let error = true"

if "let error = true", then it would throw an error because async await can not handle errors directly which is a drawback of async await, it will say that we haven't used try and catch block, as we have written code in such a way that we thought that we will always get a response but didn't prepare for the error.

//RIGHT SYNTAX
async function consumePromiseFive() {
  try {
    const response = await promiseFive;
    console.log(response);
  } catch (error) {
    console.log(error);
  }
}

consumePromiseFive();

In this example, await pauses the execution of the function until the promise is resolved or rejected, and try/catch is used to handle errors.

Using Promises with Fetch

The fetch API is commonly used to make network requests, and it returns a promise. Let's see how to handle it with both async/await and .then()/.catch().

Using Async/Await

async function getAllUsers() {

    //fetch return a promise, also here we are making a network request and 
    //such req take time, therefore we will use await and store the response
    //inside a variable
    const response = await fetch('https://jsonplaceholder.typicode.com/users');

    //as data is coming in form of string, we are converting it to json format
    const data = response.json(); 
    console.log(data);
}

getAllUsers();

//outputs: we will not get any output as we are not using try and catch block here
//now let's use try and catch block and see the output
async function getAllUsers() {
  try {
    const response = await fetch('https://jsonplaceholder.typicode.com/users');
    const data = response.json();
    console.log(data);
  } catch (error) {
    console.log("Error: ", error);
  }
}

getAllUsers();

//outputs: still we won't get any output, even we used try and catch

//REASON: Some task take time to get performed
//response.json() is taking time, therefore we will use await
async function getAllUsers() {
  try {
    const response = await fetch('https://jsonplaceholder.typicode.com/users');
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.log("Error: ", error);
  }
}

getAllUsers();

//output: We got the data printed here

Using .then()/.catch()

fetch('https://jsonplaceholder.typicode.com/users').then((response)=>{
    return response.json();
}).then((data)=>{
    console.log(data);
}).catch((error)=>{
    console.log(error);
})

//output: data get's printed here

Common Pitfalls and How to Avoid Them

  1. Forgetting to call resolve() or reject(): This will leave the promise in a pending state forever.
const promise = new Promise((resolve, reject) => {
  // Some async task
  resolve(); // Make sure to call resolve or reject
});
  1. Not handling errors properly: Always use .catch() or try/catch with async/await to handle errors.
promise
  .then((result) => {
    // Handle success
  })
  .catch((error) => {
    // Handle error
  });
  1. Using async/await without try/catch: This will cause uncaught errors.
async function asyncFunction() {
  try {
    const result = await promise;
  } catch (error) {
    // Handle error
  }
}

Wrap Up

Promises are a robust and flexible way to handle asynchronous operations in JavaScript. They help to keep your code clean and readable, and with the addition of async/await, they make asynchronous programming almost as easy as synchronous programming. By understanding and handling the states and errors properly, you can effectively manage your asynchronous code.