Open source · npm package

retry-timeout-pagination

Small, dependency-free TypeScript utilities for reliable asynchronous work — retry with exponential backoff, timeout with AbortSignal, and offset/cursor pagination.

Zero dependencies TypeScript-first ESM + CJS MIT licensed
$ npm install retry-timeout-pagination

This package is intentionally small. It gives you primitives — not a particular HTTP client, framework, or API shape — so you can drop it into whatever you're already building.

3core utilities
0runtime deps
2module formats
The toolkit

Three primitives. One job each.

Compose them together for production API clients, or use each one on its own wherever async work needs to be more resilient.

01RETRY
BACKOFF · JITTER

retry()

Retries a failing operation with exponential backoff and optional jitter. Decide exactly which errors are worth retrying with retryOn, and cancel the whole sequence with an AbortSignal.

02TIMEOUT
ABORTSIGNAL

timeout()

Races an operation against a timer and propagates an AbortSignal into it, so the underlying fetch or task is actually cancelled — not just abandoned.

03PAGINATION
OFFSET · CURSOR

paginateOffset() / paginateCursor()

Walks paginated APIs of either shape — offset/limit or cursor-based — and stops automatically on empty pages, exhausted totals, or a repeated cursor.

Quick start

Import what you need.

Ships as ESM and CommonJS with generated TypeScript declarations, so it works the same way whichever module system your project uses.

ESM
import { retry, timeout, paginateCursor, paginateOffset } from "retry-timeout-pagination";
CommonJS
const { retry, timeout, paginateCursor, paginateOffset } = require("retry-timeout-pagination");
const data = await retry( async () => { const res = await fetch("https://api.example.com/data"); // throw on non-2xx, retry() catches it if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json(); }, { maxAttempts: 4, delayMs: 500, maxDelayMs: 5000, jitter: true } );
01 · Retry

Retry only what's worth retrying.

A good production rule: retry network errors, HTTP 429 and 5xx — not most 4xx validation or auth failures. retryOn gives you that control per-call.

✓Exponential backoff with optional jitter
✓Custom retryOn predicate per error
✓Cancel the whole sequence via signal
✓Throws RetryExhaustedError with attempts + last error
02 · Timeout

Actually cancel the work, not just the wait.

timeout() races your operation against a timer and passes an AbortSignal into it — so a slow fetch gets aborted, not left running in the background.

✓Propagates a real AbortSignal into your operation
✓Throws a typed TimeoutError with the duration
✓Accepts a caller-owned signal for parent cancellation
const data = await timeout( async (signal) => { const res = await fetch(url, { signal }); if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json(); }, 5000 // ms );
const users = await paginateCursor( async ({ cursor, signal }) => { const url = new URL("https://api.example.com/users"); if (cursor) url.searchParams.set("cursor", cursor); const res = await fetch(url, { signal }); return res.json(); // { items, nextCursor } } );
03 · Pagination

Works with either API shape.

paginateOffset() handles offset/limit APIs and expects { items, total? } back. paginateCursor() handles cursor-based APIs and expects { items, nextCursor? }.

✓Stops on empty page, short page, or exhausted total
✓maxPages guard for both strategies
✓Detects a repeated cursor and throws PaginationError instead of looping forever
Composing them

Built to stack.

A typical production client wraps each page fetch in a timeout, and each timeout attempt in a retry.

paginateCursor()
↓ fetch each page
timeout()
↓ bound each attempt
retry()
↓ transient failures only
fetch()

One AbortSignal flows through all three layers, so cancelling the outer pagination call cancels everything underneath it.

✓Don't blindly retry 400/401/403/404, or non-idempotent writes
✓For POST, check idempotency keys before enabling retries
const users = await paginateCursor( async ({ cursor, signal }) => { return timeout( async (timeoutSignal) => { return retry( async () => { const url = new URL("https://api.example.com/users"); if (cursor) url.searchParams.set("cursor", cursor); const res = await fetch(url, { signal: timeoutSignal }); if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json(); }, { maxAttempts: 3, signal } ); }, 5000, { signal } ); } );
API reference

retry() options

Full option list for retry() — see the README for timeout(), paginateOffset() and paginateCursor().

Option Default Description
maxAttempts 3 Total attempts, including the first
delayMs 250 Base exponential backoff
maxDelayMs 30000 Maximum calculated delay
jitter true Randomizes delay from 0 to the calculated maximum
retryOn all errors Decide whether a given error is retryable
onRetry — Observe retry attempts as they happen
signal — Cancel the retry sequence
delay — Fully customize the retry delay function

Want the full docs?

The README covers error types, cancellation, and every option in detail — or open an issue on GitLab if something's missing.

View source ↗
Let's work together

Have a project in mind?

Tell me a little about your project, idea or business. I'll get back to you as soon as possible.

in 𝕏 ◎ f ◔ ✓