Skip to the code
cf.

Poll until a condition holds

MediumTiming

Write pollUntil(check, { interval, timeout }). It returns a promise and calls check() right away, during the call. If check returns a truthy value, the promise resolves with it. Otherwise it calls check again interval ms later, and so on.

check may be async. If it returns a promise, wait for it and use the value it resolves to, and start the interval wait only after it has settled, so checks never overlap.

If timeout ms pass from the call without a truthy value, the promise rejects with an Error whose message is "Timed out". If check throws or rejects, the promise rejects with that error. Once the promise has settled, for any of these reasons, check is never called again.

const job = await pollUntil(
  () => fetch("/api/jobs/42").then((response) => response.json()).then((job) => job.done && job),
  { interval: 1000, timeout: 30000 },
);

What the tests check

  1. calls check right away
    useFakeTimers();
    const check = spy(() => false);
    pollUntil(check, { interval: 100, timeout: 1000 }).catch(() => {});
    expect(check).toHaveBeenCalledTimes(1);
    
  2. checks again every interval ms
    const clock = useFakeTimers();
    const check = spy(() => false);
    pollUntil(check, { interval: 100, timeout: 10000 }).catch(() => {});
    await clock.tickAsync(99);
    expect(check).toHaveBeenCalledTimes(1);
    await clock.tickAsync(1);
    expect(check).toHaveBeenCalledTimes(2);
    await clock.tickAsync(300);
    expect(check).toHaveBeenCalledTimes(5);
    
  3. resolves with the first truthy value an async check gives
    const clock = useFakeTimers();
    const answers = [0, null, "ready"];
    const check = spy(async () => answers.shift());
    let result;
    pollUntil(check, { interval: 100, timeout: 10000 }).then((value) => {
      result = value;
    }, () => {});
    await clock.tickAsync(199);
    expect(result).toBeUndefined();
    await clock.tickAsync(1);
    expect(result).toBe("ready");
    
  4. stops checking once it has a value
    const clock = useFakeTimers();
    const answers = [false, "done"];
    const check = spy(() => answers.shift());
    pollUntil(check, { interval: 100, timeout: 10000 }).catch(() => {});
    await clock.tickAsync(100);
    expect(check).toHaveBeenCalledTimes(2);
    await clock.tickAsync(1000);
    expect(check).toHaveBeenCalledTimes(2);
    
  5. rejects with "Timed out" when timeout ms pass
    const clock = useFakeTimers();
    let error;
    pollUntil(() => false, { interval: 100, timeout: 250 }).catch((reason) => {
      error = reason;
    });
    await clock.tickAsync(249);
    expect(error).toBeUndefined();
    await clock.tickAsync(1);
    expect(error).toBeInstanceOf(Error);
    expect(error.message).toBe("Timed out");
    
  6. stops checking after it times out
    const clock = useFakeTimers();
    const check = spy(() => false);
    pollUntil(check, { interval: 100, timeout: 250 }).catch(() => {}); // it times out; this test only counts the checks
    await clock.tickAsync(250);
    expect(check).toHaveBeenCalledTimes(3);
    await clock.tickAsync(1000);
    expect(check).toHaveBeenCalledTimes(3);
    
  7. waits for an async check before the next interval
    const clock = useFakeTimers();
    const check = spy(() => new Promise((resolve) => setTimeout(() => resolve(false), 150)));
    pollUntil(check, { interval: 100, timeout: 10000 }).catch(() => {});
    await clock.tickAsync(249);
    expect(check).toHaveBeenCalledTimes(1);
    await clock.tickAsync(1);
    expect(check).toHaveBeenCalledTimes(2);
    
  8. rejects with the error check throws, and stops
    const clock = useFakeTimers();
    const check = spy(() => {
      if (check.callCount === 2) throw new TypeError("bad response");
      return false;
    });
    let error;
    pollUntil(check, { interval: 100, timeout: 10000 }).catch((reason) => {
      error = reason;
    });
    await clock.tickAsync(100);
    expect(error).toBeInstanceOf(TypeError);
    await clock.tickAsync(1000);
    expect(check).toHaveBeenCalledTimes(2);
    

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).