p-retry

Retry a promise-returning or async function with exponential backoff and fine-grained retry control.

Library
npm
v8.0.1
1,037 stars
MIT License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum. How we score it →
54 /100 Fair
Development Activity 44
Maintenance 32
Community 52
Maturity 60
Momentum 28

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation. How we score it →
78 /100 Good
Architecture 90
Code Quality 92
Innovation 80
Learning Curve 50

p-retry wraps any promise-returning or async function so that a failed call is retried automatically instead of propagating the first rejection. It applies exponential backoff by default, and exposes three independent hooks — shouldConsumeRetry, onFailedAttempt, and shouldRetry — so callers can decide whether a failure counts against the retry budget, react to each attempt (logging, custom delays), and decide whether to keep retrying at all, all without touching the core loop.

Beyond the default export, the package ships a makeRetriable helper that wraps an existing function so every call to it is retried transparently, and an AbortError class for signalling that retries should stop immediately with no further callbacks invoked. Retry timing integrates with AbortSignal for cancellation and with a monotonic clock (performance.now()) for maxRetryTime, so retries stop cleanly under both explicit cancellation and elapsed-time budgets regardless of system clock changes.

What You Get

  • A pRetry(input, options) function that retries a promise-returning function with configurable retries, factor, minTimeout, maxTimeout, and randomized jitter
  • Three composable hooks — shouldConsumeRetry, onFailedAttempt, shouldRetry — for controlling retry budget consumption, side effects on failure, and retry eligibility independently
  • An AbortError class for aborting all retries immediately from inside the retried function, with no further hooks invoked
  • AbortSignal support for cancelling in-flight retries from outside the retry loop
  • A maxRetryTime option measured with a monotonic clock so time budgets are immune to system clock adjustments
  • A makeRetriable(function, options) helper that wraps any function so every call is automatically retried without repeating options at each call site

Common Use Cases

  • Retrying flaky network requests (fetch calls) while treating 404s or other definitive failures as non-retryable via AbortError
  • Wrapping a database or API client call so transient errors are retried with backoff but validation errors fail immediately
  • Building a resilient job worker that retries a task a bounded number of times before giving up and reporting the last error
  • Retrying an operation only up to a wall-clock time budget (maxRetryTime) rather than a fixed attempt count

Under The Hood

Architecture

p-retry is a single-file module (index.js, under 300 lines) exporting a default async function pRetry, a named makeRetriable higher-order wrapper, and an AbortError class. The core loop in pRetry runs a bounded while loop calling the caller-supplied input(attemptNumber) function and routing any rejection through a dedicated onAttemptFailure helper that sequences shouldConsumeRetry, then onFailedAttempt, then shouldRetry — each of which can independently veto a retry, throw to abort immediately, or return a promise to defer. Delay math (calculateDelay) and remaining-time tracking (calculateRemainingTime, built on performance.now() for a monotonic clock) are extracted into small pure functions, and delayForRetry manages the actual setTimeout/AbortSignal race. There is no persistent internal state beyond function-local closures and no dependency injection; the load-bearing abstraction is the attempt loop’s error-classification order (AbortError, then non-network TypeError, then everything else), and changing that ordering would silently change retry semantics for every consumer that relies on TypeErrors from fetch-like APIs being retried only when they represent network failures.

Tech Stack

A pure ESM package (type: module) targeting Node.js 22+, with a single runtime dependency, is-network-error, used to distinguish network TypeErrors from other TypeErrors for retry eligibility. Development tooling follows the author’s standard setup: ava as the test runner, xo (an opinionated ESLint config) for linting, tsd for testing the hand-written index.d.ts type definitions, execa for spawning subprocesses in tests, and delay as a test utility. There is no build step — index.js ships directly to npm as declared in the package’s files field. CI (GitHub Actions) runs the full npm test script (xo && ava && tsd) across Node.js 22 and 24.

Code Quality

test.js contains extensive coverage (57 distinct test cases) exercising backoff/delay math, AbortError and AbortController interplay, the ordering and interaction of the three retry hooks, non-Error throws, and the removed forever option. This is paired with tsd-based type tests validating the public type surface and xo lint enforcement in CI. Error handling is explicit and typed throughout — invalid options throw descriptive TypeError instances via small dedicated validator functions rather than failing silently or coercing bad input. Naming is consistent and self-documenting across the module.

API Design The standout design choice is splitting failure handling into three independent, composable hooks — shouldConsumeRetry (does this failure count against the budget), onFailedAttempt (side effects like logging or a manual delay), and shouldRetry (should retrying continue at all) — rather than the single catch-all callback most retry libraries expose. Combined with first-class AbortSignal support woven directly into the delay logic (not bolted on after the fact), a monotonic-clock-based maxRetryTime immune to system clock skew, and the makeRetriable wrapper for turning any function into a self-retrying one without re-specifying options per call, the public API stays small while covering cases most retry utilities require workarounds for.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers