Mastering JavaScript Promises: The Key to Asynchronous Programming

JavaScript promises are a cornerstone of modern web development, enabling efficient handling of asynchronous operations. This article will guide you through the essentials of JavaScript promises.

What is a Promise?

A JavaScript promise is an object that represents the eventual completion (or failure) of an asynchronous operation and its resulting value. Promises are particularly useful for dealing with asynchronous tasks without getting bogged down in callback hell.

Let’s say in the below snippet, we are faking an API call, and after 1sec we get the data. But as soon as the code snippet executes, it will return us an object { data: undefined }

const apiCallData = new Promise((resolve, reject)=>{
    const dataFromAPI = [1, 2, 3]
    setTimeout(()=>{
        resolve(dataFromAPI);
    }, 1000);
});

apiCallData.then((apiData)=>{
   // Do any thing here
});

After some time (async time of 1 second), this Promise Object will be filled with data.

{ data: [1, 2, 3] }

Now, once we have data in the Promise Object, the above callback(then) function will be called automatically with the data.

apiCallData.then((apiData)=>{
   // Once we have data in the Promise Object, this will be executed.
   // Do any thing here
});

Promise Object

Promise State: A promise has three states:

  • Pending: The initial state, neither fulfilled nor rejected.
  • Fulfilled: The operation was completed successfully.
  • Rejected: The operation failed.

Promise Result: response/ undefined

Basic Structure of Promise:

Here’s how you create a promise:

const promise = new Promise((resolve, reject) => {
  // Asynchronous operation
  let success = true; // This is just a simulation

  if (success) {
    resolve("Operation was successful!");
  } else {
    reject("Operation failed.");
  }
});

Using Promises: then and catch

To handle the result of a promise, you use the then method for success and the catch method for errors.

promise
  .then((message) => {
    console.log(message); // Output: Operation was successful!
  })
  .catch((error) => {
    console.error(error); // This will run if the promise is rejected
  });

Chaining Promises

Promises can be chained to handle multiple asynchronous operations in a sequence.

const firstPromise = new Promise((resolve) => {
  setTimeout(() => {
    resolve("First promise resolved.");
  }, 1000);
});

firstPromise
  .then((message) => {
    console.log(message);
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve("Second promise resolved.");
      }, 1000);
    });
  })
  .then((message) => {
    console.log(message);
  })
  .catch((error) => {
    console.error(error);
  });

Handling Multiple Promises: Promise.all and Promise.race

Promise.all: It takes an array of promises and returns a single promise that resolves when all of the promises in the array resolve, or rejects if any of the promises reject.

const promise1 = Promise.resolve("Promise 1 resolved.");
const promise2 = Promise.resolve("Promise 2 resolved.");
const promise3 = Promise.resolve("Promise 3 resolved.");

Promise.all([promise1, promise2, promise3])
  .then((messages) => {
    console.log(messages); 
    // Output: [ "Promise 1 resolved.", "Promise 2 resolved.", "Promise 3 resolved." ]
  })
  .catch((error) => {
    console.error(error);
  });

Interviewers might ask to create a polyfill for this Promise.all method, this is one of the most asked interview questions.

Promise.race: It returns a promise that resolves or rejects as soon as one of the promises in the array resolves or rejects.

const promise1 = new Promise((resolve) => {
  setTimeout(resolve, 100, "First promise resolved.");
});
const promise2 = new Promise((resolve) => {
  setTimeout(resolve, 200, "Second promise resolved.");
});

Promise.race([promise1, promise2])
  .then((message) => {
    console.log(message); 
    // Output: "First promise resolved."
  })
  .catch((error) => {
    console.error(error);
  });

Interviewers might ask to create a polyfill for this Promise.race method, This is one of the most asked interview questions.

Async/Await: Simplifying Promises

The async and await keywords provide a more concise and readable way to work with promises.

Basic Usage

An async function always returns a promise. The await keyword can only be used inside an async function and pauses the execution of the function until the promise resolves.

async function fetchData() {
  try {
    let response = await fetch("https://jsonplaceholder.typicode.com/posts/1");
    let data = await response.json();
    console.log(data);
  } catch (error) {
    console.error("Error fetching data:", error);
  }
}

fetchData();

Error Handling with Async/Await

Error handling with async/await is done using try and catch blocks.

async function performAsyncTask() {
  try {
    let result = await someAsyncFunction();
    console.log(result);
  } catch (error) {
    console.error("Error:", error);
  }
}

performAsyncTask();

Differences Between Promises and Async/Await

Promises

  • Execution: It is asynchronous in nature, which means it won’t wait till the promise is resolved/rejected. Once the promise is resolved then the “then” block will be executed.
  • Syntax: Promises use the then and catch methods to handle asynchronous operations.
  • Error Handling: Errors are handled using the catch method.

Async/Await

  • Execution: It is synchronous in nature, which means it will wait till the promise is resolved/rejected. The next code will be executed after the promise is resolved/rejected.
  • Syntax: Uses the async keyword to declare an asynchronous function and the await keyword to wait for a promise to resolve.
  • Error Handling: Errors are handled using try and catch blocks, making the code more similar to synchronous code.

Conclusion

Promises and async/await are powerful tools in JavaScript that simplify handling asynchronous operations. By understanding and utilizing these concepts, you can write more readable and maintainable code. Whether you’re fetching data from an API or performing complex asynchronous tasks, promises and async/await make your life as a developer easier and your code more efficient.