Abortable Polling in TypeScript

Polling becomes dangerous when timers outlive their owner or slow requests overlap. This loop makes ownership and cancellation explicit.

Reliability · TypeScript

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

BoundaryAction
Previous request still runningDo not start another poll
Transient read failureRecord it and continue at the next bounded interval
Authorization failureStop polling and surface the terminal state
Owner unmounted or caller cancelledAbort the active request and all future polls
Server supplies a next-check timePrefer 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.

Further reference

Browse all engineering snippets

>