DEV Community

Sagnik Ghosh
Sagnik Ghosh

Posted on

A Deep Dive into Promises and Their Real-World Applications

In modern JavaScript, asynchronous programming has become an integral part of application development. Promises play a pivotal role in managing asynchronous operations effectively. This blog delves deep into JavaScript Promises, their APIs, practical scenarios, examples, and their pros and cons.

What is a Promise in JavaScript?

A Promise in JavaScript is an object representing the eventual completion (or failure) of an asynchronous operation and its resulting value. It serves as a placeholder for the result of an operation that hasn’t completed yet but is expected in the future.

The Three States of a Promise:

  • Pending: Initial state, neither fulfilled nor rejected.

  • Fulfilled: The operation completed successfully, and the promise has a resulting value.

  • Rejected: The operation failed, and the promise has a reason for the failure.

Creating a Promise

Here’s how you can create a simple Promise:

const myPromise = new Promise((resolve, reject) => {
  const success = true; // Simulating success or failure

  if (success) {
    resolve("Operation succeeded!");
  } else {
    reject("Operation failed!");
  }
});
Enter fullscreen mode Exit fullscreen mode

Consuming Promises

Promises are consumed using .then(), .catch(), and .finally():

myPromise
  .then(result => {
    console.log(result); // Logs: "Operation succeeded!"
  })
  .catch(error => {
    console.error(error); // Logs: "Operation failed!"
  })
  .finally(() => {
    console.log("Operation completed.");
  });

Enter fullscreen mode Exit fullscreen mode

Promise APIs in Depth

  • Promise.resolve(): This method creates a promise that is immediately resolved with the provided value. It is useful for wrapping non-promise values into a promise for consistency.
Promise.resolve("Quick resolve").then(value => console.log(value));
// Logs: "Quick resolve"
Enter fullscreen mode Exit fullscreen mode
  • Promise.reject(): This method creates a promise that is immediately rejected with the provided reason. It’s typically used to simulate errors or pass rejections in promise chains.
Promise.reject("Quick reject").catch(reason => console.error(reason));
// Logs: "Quick reject"
Enter fullscreen mode Exit fullscreen mode
  • Promise.all(): This method takes an array of promises and returns a single promise that resolves when all promises have resolved or rejects as soon as one promise rejects. It is often used when multiple asynchronous tasks need to complete together.
const promises = [
  Promise.resolve(1),
  Promise.resolve(2),
  Promise.resolve(3)
];

Promise.all(promises).then(values => console.log(values));
// Logs: [1, 2, 3]
Enter fullscreen mode Exit fullscreen mode
  • Promise.allSettled(): This method takes an array of promises and returns a promise that resolves after all promises have settled, regardless of whether they are resolved or rejected. It is useful when you want to handle both outcomes for all promises.
const promises = [
  Promise.resolve(1),
  Promise.reject("Error"),
  Promise.resolve(3)
];

Promise.allSettled(promises).then(results => console.log(results));
/* Logs:
[
  { status: "fulfilled", value: 1 },
  { status: "rejected", reason: "Error" },
  { status: "fulfilled", value: 3 }
]
*/
Enter fullscreen mode Exit fullscreen mode
  • Promise.race(): This method takes an array of promises and returns a single promise that resolves or rejects as soon as the first promise in the array resolves or rejects. It’s ideal for timeout scenarios.
const promises = [
  new Promise(resolve => setTimeout(() => resolve("Fast"), 100)),
  new Promise(resolve => setTimeout(() => resolve("Slow"), 500))
];

Promise.race(promises).then(value => console.log(value));
// Logs: "Fast"
Enter fullscreen mode Exit fullscreen mode
  • Promise.any() This method takes an array of promises and resolves with the value of the first promise that fulfills. If all promises are rejected, it returns an AggregateError. It’s useful when you only care about the first successful outcome.
const promises = [
  Promise.reject("Error"),
  Promise.resolve("First success"),
  Promise.resolve("Second success")
];

Promise.any(promises).then(value => console.log(value));
// Logs: "First success"
Enter fullscreen mode Exit fullscreen mode

Practical Scenarios

  • Fetching Data from Multiple APIs Imagine needing user information and their associated posts. You can handle both requests simultaneously:
const userPromise = fetch("https://api.example.com/user").then(res => res.json());
const postsPromise = fetch("https://api.example.com/posts").then(res => res.json());

Promise.all([userPromise, postsPromise])
  .then(([user, posts]) => {
    console.log("User:", user);
    console.log("Posts:", posts);
  })
  .catch(error => console.error("Error fetching data:", error));
Enter fullscreen mode Exit fullscreen mode
  • Sequential Execution of Dependent Promises Some operations depend on the completion of prior tasks. Here’s an example:
fetch("https://api.example.com/authenticate")
  .then(response => response.json())
  .then(authData => {
    return fetch(`https://api.example.com/user/${authData.userId}`);
  })
  .then(response => response.json())
  .then(userData => console.log("User Data:", userData))
  .catch(error => console.error("Error:", error));
Enter fullscreen mode Exit fullscreen mode
  • Retry Mechanism To handle transient network errors, you can implement a retry mechanism:
function retryPromise(fn, retries) {
  return fn().catch(err => {
    if (retries > 0) {
      return retryPromise(fn, retries - 1);
    } else {
      throw err;
    }
  });
}

retryPromise(() => fetch("https://api.example.com/data"), 3)
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(err => console.error("Failed after retries:", err));
Enter fullscreen mode Exit fullscreen mode
  • Timeout Handling Handling operations that take too long:
function fetchWithTimeout(url, timeout) {
  return Promise.race([
    fetch(url),
    new Promise((_, reject) => setTimeout(() => reject("Request timed out"), timeout))
  ]);
}

fetchWithTimeout("https://api.example.com/data", 5000)
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(err => console.error(err));
Enter fullscreen mode Exit fullscreen mode
  • Chaining Multiple Asynchronous Tasks For tasks requiring step-by-step execution:
function task1() {
  return new Promise(resolve => setTimeout(() => resolve("Task 1 completed"), 1000));
}

function task2() {
  return new Promise(resolve => setTimeout(() => resolve("Task 2 completed"), 2000));
}

task1()
  .then(result1 => {
    console.log(result1);
    return task2();
  })
  .then(result2 => {
    console.log(result2);
  });
Enter fullscreen mode Exit fullscreen mode

Pros and Cons of Promises

Pros:

  • Readable Code: Promises make asynchronous code more readable compared to nested callbacks.

  • Error Handling: Centralized error handling with .catch().

  • Chaining: Easy to chain multiple asynchronous operations.

Cons:

  • Complexity: Understanding edge cases with .race(), .all(), or .allSettled() can be challenging.

  • Debugging: Tracing the source of errors can sometimes be difficult in complex promise chains.

Top comments (0)