Language: EN

javascript-promesas

Promises in JavaScript

In JavaScript, promises are a fundamental mechanism to facilitate asynchronous programming.

They allow us to manage operations that do not complete immediately (such as network requests, file reading operations, or timers) in a more structured and less error-prone way compared to callbacks.

A promise is like a “container” for a value that may be available now, in the future, or never (that’s why it’s called a promise)

In general, it is advisable to use async/await to work with promises in a more synchronous manner. This can make asynchronous code easier to understand and maintain.

How a Promise Works

A promise is an object that represents the possible success or failure of an asynchronous operation.

To reflect this, during its execution the promise has a property State, which can be one of the following:

  • Pending: Initial state. The asynchronous operation has not yet completed.
  • Fulfilled: The operation has completed successfully, and the promise has a resulting value.
  • Rejected: The operation failed, and the promise has a reason for the error.

Creating a Promise

We can create a promise using the Promise constructor. This constructor takes a function (called executor) that receives two functions as arguments:

  • resolve is called if the operation is successful, and the promise’s state changes to fulfilled.
  • reject is called if there is an error, and the state changes to rejected.

These arguments are functions that are called to change the state of the promise. Let’s see it with an example,

const myPromise = new Promise((resolve, reject) => {
  // Simulating an asynchronous operation
  let success = true; // Simulates the outcome of the operation

  if (success) {
    resolve('Operation successful');
  } else {
    reject('Error in the operation');
  }
});

In this example,

  • We create a promise that receives its two functions resolve and reject.
  • We simulate performing an operation with success.
  • Depending on the outcome of the operation, we invoke either resolve or reject.

Handling Promises

To handle the result of a promise, we typically use the methods .then() and .catch():

myPromise.then(onFulfilled, onRejected)

This method is called when the promise is fulfilled or rejected.

  • onFulfilled is a function that is called with the value of the promise when it is fulfilled,
  • onRejected is called with the reason for rejection if the promise fails.
myPromise.catch(onRejected)

This method is used to handle errors. It is a shortcut for .then(null, onRejected) and is called when the promise is rejected.

For example, let’s see an example of how a promise would typically be used,

myPromise
  .then(result => {
    console.log(result); // 'Operation successful'
  })
  .catch(error => {
    console.error(error); // 'Error in the operation'
  });

Chaining Promises

Promises allow chaining multiple .then() to handle sequences of asynchronous operations.

Each .then() returns a new promise, making it easier to compose asynchronous tasks.

myPromise
  .then(result => {
    console.log(result); // 'Operation successful'
    return 'Another value';
  })
  .then(anotherResult => {
    console.log(anotherResult); // 'Another value'
  })
  .catch(error => {
    console.error(error);
  });

Nested Promises

We could also nest promises within other promises to manage operations that depend on previous results.

myPromise
  .then(result => {
    console.log(result); // 'Operation successful'
    return new Promise((resolve, reject) => {
      setTimeout(() => resolve('Additional asynchronous operation'), 1000);
    });
  })
  .then(additional => {
    console.log(additional); // 'Additional asynchronous operation'
  })
  .catch(error => {
    console.error(error);
  });

However, this can lead to hard-to-read code, so it’s preferable to use chaining whenever possible.

Static Promise Methods

JavaScript provides several static methods on the Promise object to handle multiple promises simultaneously.

These methods return a new promise, which is a composition of the promises passed as arguments.

Promise.all()

Promise.all() takes an iterable of promises and returns a new promise that resolves when all promises in the iterable are fulfilled. If any promise is rejected, the returned promise will be rejected immediately.

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

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

Promise.race()

Promise.race() returns a promise that resolves or rejects as soon as one of the promises in the iterable resolves or rejects.

const slowPromise = new Promise(resolve => setTimeout(() => resolve('Slow'), 2000));
const fastPromise = new Promise(resolve => setTimeout(() => resolve('Fast'), 1000));

Promise.race([slowPromise, fastPromise])
  .then(result => {
    console.log(result); // 'Fast'
  });

Promise.allSettled()

Promise.allSettled() returns a promise that resolves when all promises in the iterable have settled (whether they have been fulfilled or rejected).

const promise1 = Promise.resolve(1);
const promise2 = Promise.reject('Error');
const promise3 = Promise.resolve(3);

Promise.allSettled([promise1, promise2, promise3])
  .then(results => {
    console.log(results);
    // [{status: 'fulfilled', value: 1}, {status: 'rejected', reason: 'Error'}, {status: 'fulfilled', value: 3}]
  });

It returns an array of objects that describe the outcome of each promise.

Promise.any()

Promise.any() returns a promise that resolves as soon as one of the promises in the iterable resolves.

const promise1 = Promise.reject('Error 1');
const promise2 = Promise.reject('Error 2');
const promise3 = Promise.resolve('Success');

Promise.any([promise1, promise2, promise3])
  .then(result => {
    console.log(result); // 'Success'
  });

If all promises are rejected, the returned promise will be rejected with an AggregateError.

Practical Examples