9 Must-Know Advanced Uses of Promises

Overview

The Promise object represents the eventual completion (or failure) of an asynchronous operation and its resulting value.

A Promise is always in one of the following states:

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

Unlike “old-style” callbacks, using Promises has the following conventions:

  • Callback functions will not be called until the current event loop completes.
  • Even if the asynchronous operation completes (successfully or unsuccessfully), callbacks added via then() afterward will still be called.
  • You can add multiple callbacks by calling then() multiple times, and they will be executed in the order they were added.

The characteristic feature of Promises is chaining.

Usage

  1. Promise.all([])

When all Promise instances in the array succeed, it returns an array of success results in the order they were requested. If any Promise fails, it enters the failure callback.

const p1 = new Promise((resolve) => {
  resolve(1);
});
const p2 = new Promise((resolve) => {
  resolve(1);
});
const p3 = Promise.resolve("ok");

// If all promises succeed, result will be an array of 3 results.
const result = Promise.all([p1, p2, p3]);
// If one fails, the result is the failed promise's value.

2. Promise.allSettled([])

The execution will not fail; it returns an array corresponding to the status of each Promise instance in the input array.

const p1 = Promise.resolve(1);
const p2 = Promise.reject(-1);
Promise.allSettled([p1, p2]).then((res) => {
  console.log(res);
});
// Output:
/*
   [
    { status: 'fulfilled', value: 1 },
    { status: 'rejected', reason: -1 }
   ] 
*/

3. Promise.any([])

If any Promise in the input array fulfills, the returned instance will become fulfilled and return the value of the first fulfilled promise. If all are rejected, it will become rejected.

const p1 = new Promise((resolve, reject) => {
  reject(1);
});
const p2 = new Promise((resolve, reject) => {
  reject(2);
});
const p3 = Promise.resolve("ok");

Promise.any([p1, p2, p3]).then(
  (r) => console.log(r), // Outputs 'ok'
  (e) => console.log(e)
);

4. Promise.race([])

As soon as any Promise in the array changes state, the state of the race method will change accordingly; the value of the first changed Promise will be passed to the race method’s callback.

const p1 = new Promise((resolve) => {
  setTimeout(() => {
    resolve(10);
  }, 3000);
});
const p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    throw new Error("I encountered an error");
  }, 2000);
});

Promise.race([p1, p2]).then(
  (v) => console.log(v), // Outputs 10
  (e) => console.log(e)
);

Throwing an exception does not change the race state; it is still determined by p1.

Advanced Uses

Here are 9 advanced uses that help developers handle asynchronous operations more efficiently and elegantly.

  1. Concurrency Control

Using Promise.all allows for parallel execution of multiple Promises, but to control the number of simultaneous requests, you can implement a concurrency control function.

const concurrentPromises = (promises, limit) => {
  return new Promise((resolve, reject) => {
    let i = 0;
    let result = [];
    const executor = () => {
      if (i >= promises.length) {
        return resolve(result);
      }
      const promise = promises[i++];
      Promise.resolve(promise)
        .then((value) => {
          result.push(value);
          if (i < promises.length) {
            executor();
          } else {
            resolve(result);
          }
        })
        .catch(reject);
    };
    for (let j = 0; j < limit && j < promises.length; j++) {
      executor();
    }
  });
};

2. Promise Timeout

Sometimes, you may want a Promise to automatically reject if it does not resolve within a certain time frame. This can be implemented as follows.

const promiseWithTimeout = (promise, ms) =>
  Promise.race([
    promise,
    new Promise((resolve, reject) =>
      setTimeout(() => reject(new Error("Timeout after " + ms + "ms")), ms)
    ),
  ]);

3. Cancelling Promises

Native JavaScript Promises cannot be cancelled, but you can simulate cancellation by introducing controllable interrupt logic.

const cancellablePromise = (promise) => {
  let isCanceled = false;
  const wrappedPromise = new Promise((resolve, reject) => {
    promise.then(
      (value) => (isCanceled ? reject({ isCanceled, value }) : resolve(value)),
      (error) => (isCanceled ? reject({ isCanceled, error }) : reject(error))
    );
  });
  return {
    promise: wrappedPromise,
    cancel() {
      isCanceled = true;
    },
  };
};

4. Sequential Execution of Promise Array

Sometimes you need to execute a series of Promises in order, ensuring that the previous asynchronous operation completes before starting the next.

const sequencePromises = (promises) =>
  promises.reduce((prev, next) => prev.then(() => next()), Promise.resolve());

5. Retry Logic for Promises

When a Promise is rejected due to temporary errors, you may want to retry its execution.

const retryPromise = (promiseFn, maxAttempts, interval) => {
  return new Promise((resolve, reject) => {
    const attempt = (attemptNumber) => {
      if (attemptNumber === maxAttempts) {
        reject(new Error("Max attempts reached"));
        return;
      }
      promiseFn()
        .then(resolve)
        .catch(() => {
          setTimeout(() => {
            attempt(attemptNumber + 1);
          }, interval);
        });
    };
    attempt(0);
  });
};

6. Ensuring a Promise Resolves Only Once

In some cases, you may want to ensure that a Promise resolves only once, even if resolve is called multiple times.

const onceResolvedPromise = (executor) => {
  let isResolved = false;
  return new Promise((resolve, reject) => {
    executor((value) => {
      if (!isResolved) {
        isResolved = true;
        resolve(value);
      }
    }, reject);
  });
};

7. Using Promises Instead of Callbacks

Promises provide a more standardized and convenient way to handle asynchronous operations by replacing callback functions.

const callbackToPromise = (fn, ...args) => {
  return new Promise((resolve, reject) => {
    fn(...args, (error, result) => {
      if (error) {
        reject(error);
      } else {
        resolve(result);
      }
    });
  });
};

8. Dynamically Generating a Promise Chain

In some situations, you may need to dynamically create a series of Promise chains based on different conditions.

const tasks = [task1, task2, task3]; // Array of asynchronous tasks

const promiseChain = tasks.reduce((chain, currentTask) => {
  return chain.then(currentTask);
}, Promise.resolve());

9. Using Promises to Implement a Simple Asynchronous Lock

In a multi-threaded environment, you can use Promises to implement a simple asynchronous lock, ensuring that only one task can access shared resources at a time.

let lock = Promise.resolve();

const acquireLock = () => {
  let release;
  const waitLock = new Promise((resolve) => {
    release = resolve;
  });
  const tryAcquireLock = lock.then(() => release);
  lock = waitLock;
  return tryAcquireLock;
};

This code creates and resolves Promises continuously, implementing a simple FIFO queue to ensure that only one task can access shared resources. The lock variable represents whether there is a task currently executing, always pointing to the Promise of the task in progress. The acquireLock function requests permission to execute and creates a new Promise to wait for the current task to finish.

Conclusion

Promises are an indispensable part of modern JavaScript asynchronous programming. Mastering their advanced techniques will greatly enhance development efficiency and code quality. With the various methods outlined above, developers can handle complex asynchronous scenarios more confidently and write more readable, elegant, and robust code.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *