Polling means asking the same question again and again, on a timer, until you get the answer you're waiting for. You poll when nothing will tell you that something has changed: you started a video export on the server and want to know when it's ready, or a payment is waiting to be confirmed. So you ask the server every second or two until the answer is yes.
It's the child in the back of the car asking "are we there yet?" every few minutes. Each question costs a little, and most of the answers are no.
Here's a job checked every 1000 ms, where each check is a request that usually takes 200 ms. The wait for the next check starts when the answer to the last one arrives:
| Time | What happens |
|---|---|
| 0 ms | Check 1 is sent. |
| 200 ms | Answer: still running. Wait 1000 ms. |
| 1200 ms | Check 2 is sent. The server is busy, so this one is slow. |
| 2700 ms | Answer: still running. Wait 1000 ms. |
| 3700 ms | Check 3 is sent. |
| 3900 ms | Answer: done. Stop polling and use the result. |
Waiting from each answer, rather than checking on a fixed beat, means a slow check pushes the next one back instead of overlapping it. On a fixed beat of every 1000 ms, the slow check 2 would still be waiting for its answer when check 3 went out: two requests at once to a server that's already slow, with answers that could arrive out of order.
Polling also needs a time limit, a timeout, after which it gives up with an error. The job may have failed without saying so, or the server may be down, and nobody should watch a spinner forever.
When the server can tell you about changes itself, that's usually better than asking: a WebSocket keeps a two-way connection open, and server-sent events (EventSource) let the server send updates down a response it keeps open. Polling works with any server that answers ordinary requests, which is why it's still common.
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 },
);
useFakeTimers();
const check = spy(() => false);
pollUntil(check, { interval: 100, timeout: 1000 }).catch(() => {});
expect(check).toHaveBeenCalledTimes(1);
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);
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");
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);
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");
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);
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);
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);
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).
I'm here to help with Poll until a condition holds. I give hints, never the solution, so you get to solve it yourself. Ask for a hint, select code to ask about it, or press Ask Tuto why on a failing test.
Your first 3 answers need no account. for free to keep asking.