Skip to the code
cf.

Promise.race

Write promiseRace(iterable), which works like Promise.race without calling it. It returns a promise that settles the same way as the first input to settle: it fulfills with that input's value, or rejects with its reason. Whatever the other inputs do later doesn't matter.

Inputs can be promises or plain values; a plain value counts as already fulfilled. iterable can be any iterable, such as an array or a Set. With an empty iterable, the promise stays pending forever, like the built-in.

const answer = await promiseRace([
  fetchJson("/api/prices"),
  new Promise((resolve) => setTimeout(resolve, 50, "cached prices")),
]);
// "cached prices" if the timer wins, the response if the fetch is faster

What the tests check

  1. fulfills with the first value to arrive
    const clock = useFakeTimers();
    const after = (ms, value) => new Promise((resolve) => setTimeout(resolve, ms, value));
    let result;
    promiseRace([after(300, "slow"), after(100, "fast"), after(200, "medium")]).then((value) => {
      result = value;
    });
    await clock.tickAsync(300);
    expect(result).toBe("fast");
    
  2. rejects when the first input to settle rejects
    const clock = useFakeTimers();
    const error = new Error("timed out");
    const slow = new Promise((resolve) => setTimeout(resolve, 200, "data"));
    const failing = new Promise((_, reject) => setTimeout(reject, 100, error));
    failing.catch(() => {}); // mark it handled, so it's never reported as unhandled
    let reason;
    promiseRace([slow, failing]).catch((e) => {
      reason = e;
    });
    await clock.tickAsync(200);
    expect(reason).toBe(error);
    
  3. ignores a rejection that comes after the winner
    const clock = useFakeTimers();
    const fast = new Promise((resolve) => setTimeout(resolve, 100, "data"));
    const late = new Promise((_, reject) => setTimeout(reject, 200, new Error("too late")));
    late.catch(() => {}); // mark it handled, so it's never reported as unhandled
    let outcome;
    promiseRace([fast, late]).then(
      (value) => {
        outcome = value;
      },
      () => {
        outcome = "rejected";
      },
    );
    await clock.tickAsync(200);
    expect(outcome).toBe("data");
    
  4. treats plain values as already fulfilled
    const clock = useFakeTimers();
    const later = new Promise((resolve) => setTimeout(resolve, 100, "later"));
    let result;
    promiseRace([later, 42]).then((value) => {
      result = value;
    });
    await clock.tickAsync(100);
    expect(result).toBe(42);
    
  5. accepts any iterable, such as a Set
    const clock = useFakeTimers();
    const after = (ms, value) => new Promise((resolve) => setTimeout(resolve, ms, value));
    let result;
    promiseRace(new Set([after(200, "b"), after(100, "a")])).then((value) => {
      result = value;
    });
    await clock.tickAsync(200);
    expect(result).toBe("a");
    
  6. stays pending for an empty iterable
    let settled = false;
    promiseRace([]).then(
      () => {
        settled = true;
      },
      () => {
        settled = true;
      },
    );
    await flush();
    expect(settled).toBe(false);
    
  7. doesn't call the built-in Promise.race
    const builtIn = Promise.race;
    Promise.race = () => {
      throw new Error("promiseRace called the built-in Promise.race");
    };
    let promise;
    try {
      promise = promiseRace([Promise.resolve("mine")]);
    } finally {
      Promise.race = 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).