Skip to the code
cf.

Promise.any

Write promiseAny(iterable), which works like Promise.any without calling it. It fulfills with the value of the first input to fulfill, and ignores rejections as long as another input might still fulfill.

If every input rejects, it rejects with an AggregateError whose errors array holds the reasons in input order, not the order they rejected in. An empty iterable rejects right away, with an AggregateError whose errors is []. Inputs can be plain values, which count as fulfilled, and iterable can be any iterable.

const fastest = await promiseAny([fetchJson(mirrorA), fetchJson(mirrorB)]);
// the first mirror to answer; one mirror being down doesn't matter
 
try {
  await promiseAny([Promise.reject(new Error("a")), Promise.reject(new Error("b"))]);
} catch (error) {
  error instanceof AggregateError; // true
  error.errors; // [Error("a"), Error("b")]
}

What the tests check

  1. fulfills with the first input to fulfill
    const clock = useFakeTimers();
    const after = (ms, value) => new Promise((resolve) => setTimeout(resolve, ms, value));
    let result;
    promiseAny([after(300, "slow"), after(100, "fast")]).then((value) => {
      result = value;
    });
    await clock.tickAsync(300);
    expect(result).toBe("fast");
    
  2. ignores rejections while another input can still fulfill
    const clock = useFakeTimers();
    const failing = new Promise((_, reject) => setTimeout(reject, 50, new Error("mirror A is down")));
    failing.catch(() => {}); // mark it handled, so it's never reported as unhandled
    const ok = new Promise((resolve) => setTimeout(resolve, 100, "mirror B"));
    let outcome;
    promiseAny([failing, ok]).then(
      (value) => {
        outcome = value;
      },
      (error) => {
        outcome = error;
      },
    );
    await clock.tickAsync(100);
    expect(outcome).toBe("mirror B");
    
  3. rejects with an AggregateError once every input has rejected
    const clock = useFakeTimers();
    const failAfter = (ms, reason) => {
      const promise = new Promise((_, reject) => setTimeout(reject, ms, reason));
      promise.catch(() => {}); // mark it handled, so it's never reported as unhandled
      return promise;
    };
    let reason;
    promiseAny([failAfter(100, new Error("a")), failAfter(200, new Error("b"))]).catch((e) => {
      reason = e;
    });
    await clock.tickAsync(100);
    expect(reason).toBeUndefined();
    await clock.tickAsync(100);
    expect(reason).toBeInstanceOf(AggregateError);
    
  4. lists the reasons in input order, not rejection order
    const clock = useFakeTimers();
    const failAfter = (ms, reason) => {
      const promise = new Promise((_, reject) => setTimeout(reject, ms, reason));
      promise.catch(() => {}); // mark it handled, so it's never reported as unhandled
      return promise;
    };
    const first = new Error("first");
    const second = new Error("second");
    let reason;
    promiseAny([failAfter(300, first), failAfter(100, second)]).catch((e) => {
      reason = e;
    });
    await clock.tickAsync(300);
    expect(reason.errors).toHaveLength(2);
    expect(reason.errors[0]).toBe(first);
    expect(reason.errors[1]).toBe(second);
    
  5. treats plain values as fulfilled
    const failing = Promise.reject(new Error("no"));
    failing.catch(() => {}); // mark it handled, so it's never reported as unhandled
    let result;
    promiseAny([failing, 7]).then((value) => {
      result = value;
    });
    await flush();
    expect(result).toBe(7);
    
  6. rejects right away for an empty iterable
    let reason;
    promiseAny([]).catch((e) => {
      reason = e;
    });
    await flush();
    expect(reason).toBeInstanceOf(AggregateError);
    expect(reason.errors).toEqual([]);
    
  7. accepts any iterable, such as a Set
    const clock = useFakeTimers();
    const after = (ms, value) => new Promise((resolve) => setTimeout(resolve, ms, value));
    let result;
    promiseAny(new Set([after(200, "b"), after(100, "a")])).then((value) => {
      result = value;
    });
    await clock.tickAsync(200);
    expect(result).toBe("a");
    
  8. doesn't call the built-in Promise.any
    const builtIn = Promise.any;
    Promise.any = () => {
      throw new Error("promiseAny called the built-in Promise.any");
    };
    let promise;
    try {
      promise = promiseAny([Promise.resolve("mine")]);
    } finally {
      Promise.any = builtIn;
    }
    await expect(promise).resolves.toBe("mine");
    

Learn more

solution.jsSaved in this browser

Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.

Ctrl + Enter runs the tests (⌘ + Enter on a Mac).