Implement Promise.any

Promise.any resolves with the first input that fulfils, and rejects only if every input rejects. Resolve on the first success, collect rejection reasons by index, and when the count of rejections reaches the input length, reject with an AggregateError carrying them in input order.

Easy to medium promises error aggregation

The question

Almost always straight after Promise.all, because it’s the mirror image and the contrast is the point. Two details carry the marks: rejecting with an AggregateError rather than a plain Error, and rejecting empty input immediately instead of hanging.

The solution

Structurally the inverse of Promise.all: success short-circuits, failure accumulates.

function promiseAny(iterable) {
  return new Promise((resolve, reject) => {
    const items = Array.from(iterable);
    const errors = new Array(items.length);
    let remaining = items.length;

    if (remaining === 0) {
      reject(new AggregateError([], "All promises were rejected"));
      return;
    }

    items.forEach((item, index) => {
      Promise.resolve(item).then(resolve, (error) => {
        // Reasons are reported in input order, not rejection order.
        errors[index] = error;
        remaining -= 1;
        if (remaining === 0) {
          reject(new AggregateError(errors, "All promises were rejected"));
        }
      });
    });
  });
}

How it works

The tests it has to pass

This is the part most explanations leave out. "It works" is not a claim you can check by reading. These are the 4 cases Practice Pad runs against your solution, and the implementation above passes all of them.

Easy2 cases · first success wins

resolves with the first value that fulfils

const winner = await promiseAny([
  new Promise((resolve) => setTimeout(() => { resolve("later"); }, 40)),
  Promise.resolve("cached"),
]);
expectEqual(winner, "cached");

resolves with the fastest of several pending promises

const at = (ms, value) => new Promise((resolve) => setTimeout(() => { resolve(value); }, ms));
expectEqual(await promiseAny([at(60, "slow"), at(10, "quick"), at(40, "middle")]), "quick");
Medium2 cases · AggregateError behaviour

rejects empty input with an AggregateError

let isAggregate = false;
try {
  await promiseAny([]);
} catch (error) {
  isAggregate = error instanceof AggregateError;
}
expectEqual(isAggregate, true);

keeps every rejection reason in input order when all inputs reject

let messages;
try {
  await promiseAny([Promise.reject(new Error("first")), Promise.reject(new Error("second"))]);
} catch (error) {
  if (error instanceof AggregateError) {
    messages = error.errors.map((item) => (item instanceof Error ? item.message : "?"));
  }
}
expectEqual(messages, ["first", "second"]);

Edge cases to watch out for

Rejecting with a plain Error

The specification is specific: it must be an AggregateError, whose errors property holds every reason. The test does an instanceof check, and so do real callers.

Resolving empty input instead of rejecting

Promise.all([]) fulfils with [], which makes people assume Promise.any([]) does something similar. It rejects, because no input fulfilled and none ever will.

Collecting errors in rejection order

Pushing as they arrive gives you the order they failed in, which isn’t the input order the specification promises. Assign by index.

Confusing it with Promise.race

race settles with the first promise to settle either way, so a fast rejection wins. any ignores rejections until every one has failed. They behave identically only when the first settlement is a success.

Follow up questions

What’s the difference between Promise.any and Promise.race?

race settles with whichever promise settles first, success or failure, so one fast rejection beats a slower success. any ignores rejections and waits for the first fulfilment, rejecting only if every input rejects.

What is AggregateError?

A built-in error type that wraps several errors in one, exposing them on an errors array. It exists mainly for Promise.any, which needs to report why every input failed rather than picking one arbitrarily.

How do you stop Promise.any waiting forever?

Race it against a timeout, or include a promise that rejects after a delay as one of the inputs. The second is neater: because any only rejects when everything rejects, a timed rejection joins the aggregate rather than overriding a real success that arrives late.

When would you actually use Promise.any?

When several sources can answer the same question and you want the first that works: querying mirrors or CDN endpoints, racing a cache against a network fetch, or trying several fallback providers. You want a success, and you only care about the failures if they all fail.

Practise this with the tests running

Practice Pad runs these 4 cases against your own solution, in a real editor, on your Mac. Twenty-four questions, and an optional AI panel that explains why a test failed.

Download for macOS Free. Requires macOS 12 or later on Apple Silicon.

See all 24 questions