Skip to the code
cf.

Promise.allSettled

Write promiseAllSettled(iterable), which works like Promise.allSettled without calling it. It waits for every input to settle, then fulfills with one record per input, in input order: { status: "fulfilled", value } or { status: "rejected", reason }, with no other keys.

It never rejects, except with a TypeError when the argument isn't iterable (calling it doesn't throw). Inputs can be plain values, iterable can be any iterable, and an empty iterable fulfills with [].

const results = await promiseAllSettled([
  Promise.resolve(1),
  Promise.reject(new Error("nope")),
  "plain",
]);
// [
//   { status: "fulfilled", value: 1 },
//   { status: "rejected", reason: Error("nope") },
//   { status: "fulfilled", value: "plain" },
// ]

What the tests check

  1. reports each outcome in input order
    const clock = useFakeTimers();
    const error = new Error("nope");
    const slow = new Promise((resolve) => setTimeout(resolve, 200, "a"));
    const failing = new Promise((_, reject) => setTimeout(reject, 100, error));
    failing.catch(() => {}); // mark it handled, so it's never reported as unhandled
    let result;
    promiseAllSettled([slow, failing, 3]).then((value) => {
      result = value;
    });
    await clock.tickAsync(200);
    expect(result).toEqual([
      { status: "fulfilled", value: "a" },
      { status: "rejected", reason: error },
      { status: "fulfilled", value: 3 },
    ]);
    expect(result[1].reason).toBe(error);
    
  2. waits for every input, even after a rejection
    const clock = useFakeTimers();
    const failing = new Promise((_, reject) => setTimeout(reject, 100, new Error("nope")));
    failing.catch(() => {}); // mark it handled, so it's never reported as unhandled
    const slow = new Promise((resolve) => setTimeout(resolve, 300, "late"));
    let result;
    promiseAllSettled([failing, slow]).then((value) => {
      result = value;
    });
    await clock.tickAsync(299);
    expect(result).toBeUndefined();
    await clock.tickAsync(1);
    expect(result).toHaveLength(2);
    
  3. fulfills even when every input rejects
    const failed = (reason) => {
      const promise = Promise.reject(reason);
      promise.catch(() => {}); // mark it handled, so it's never reported as unhandled
      return promise;
    };
    await expect(promiseAllSettled([failed("a"), failed("b")])).resolves.toEqual([
      { status: "rejected", reason: "a" },
      { status: "rejected", reason: "b" },
    ]);
    
  4. fulfills with [] for an empty iterable
    let result;
    promiseAllSettled([]).then((value) => {
      result = value;
    });
    await flush();
    expect(result).toEqual([]);
    
  5. accepts any iterable, such as a Set
    let result;
    promiseAllSettled(new Set([1, Promise.resolve(2)])).then((value) => {
      result = value;
    });
    await flush();
    expect(result).toEqual([
      { status: "fulfilled", value: 1 },
      { status: "fulfilled", value: 2 },
    ]);
    
  6. rejects with a TypeError when the argument isn't iterable
    let promise;
    expect(() => {
      promise = promiseAllSettled(42);
    }).not.toThrow();
    await expect(promise).rejects.toThrow(TypeError);
    
  7. doesn't call the built-in Promise.allSettled
    const builtIn = Promise.allSettled;
    Promise.allSettled = () => {
      throw new Error("promiseAllSettled called the built-in Promise.allSettled");
    };
    let promise;
    try {
      promise = promiseAllSettled([Promise.resolve("mine")]);
    } finally {
      Promise.allSettled = builtIn;
    }
    await expect(promise).resolves.toEqual([{ status: "fulfilled", value: "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).