Polling becomes dangerous when timers outlive their owner or slow requests overlap. This loop makes ownership and cancellation explicit.
Abortable Polling Loop
The owner of repeated work must also own its cancellation and cadence.
function sleep(ms: number, signal: AbortSignal) {
signal.throwIfAborted()
return new Promise<void>((resolve, reject) => {
const done = () => { clearTimeout(timer); signal.removeEventListener('abort', abort) }
const abort = () => { done(); reject(signal.reason) }
const timer = setTimeout(() => { done(); resolve() }, ms)
signal.addEventListener('abort', abort, { once: true })
})
}
async function poll(load: (signal: AbortSignal) => Promise<void>, retryable: (error: unknown) => boolean, intervalMs: number, signal: AbortSignal) {
while (!signal.aborted) {
const started = Date.now()
try { await load(signal) }
catch (error) {
if (signal.aborted) throw signal.reason
if (!retryable(error)) throw error
}
await sleep(Math.max(0, intervalMs - (Date.now() - started)), signal)
}
}Invariant: At most one poll is in flight, and cancellation stops all future work.
Use when: You need fresh state without overlapping requests or an immortal timer.
Why this boundary matters
Overlapping timers create hidden concurrency; abandoned components create immortal traffic. One loop makes execution and cancellation share an owner.
Failure policy
| Boundary | Action |
|---|---|
| Previous request still running | Do not start another poll |
| Transient read failure | Record it and continue at the next bounded interval |
| Authorization failure | Stop polling and surface the terminal state |
| Owner unmounted or caller cancelled | Abort the active request and all future polls |
| Server supplies a next-check time | Prefer that signal over the local default |
Trade-offs
Polling is operationally simple but spends requests when nothing changes and can synchronize clients into bursts. Add jitter at fleet scale, honor server hints, and move to push only when the connection and delivery complexity earns its cost.
Decision rule: Poll when bounded staleness is acceptable and the request volume is cheaper than maintaining a push channel.