retry

Lightweight abstraction for exponential backoff and custom retry strategies in Node.js

Library
npm
v0.13.1
1,258stars
MIT License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
40/100Fair
Development Activity0
Maintenance20
Community52
Maturity60
Momentum28

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
70/100Good
Architecture70
Code Quality65
Innovation65
Learning Curve80

retry is a minimal, dependency-free Node.js library for wrapping unreliable operations in configurable retry logic. It exposes exponential backoff with a tunable factor, timeout bounds, and randomization, plus a lower-level RetryOperation API for full control over attempt counts and accumulated errors.

Instead of hand-rolling setTimeout loops around flaky network calls or DNS lookups, developers call retry.operation() and let the library manage the schedule of timeouts, collect errors, and expose the most-frequently-seen error once all attempts are exhausted. It has shipped inside countless higher-level Node.js libraries as a foundational retry primitive since 2011.

What You Get

  • Exponential backoff timeout calculation via retry.timeouts() and retry.createTimeout()
  • A stateful RetryOperation object that tracks attempts, collects errors, and exposes mainError()
  • A retry.wrap() helper that automatically retries every method on an existing object
  • Configurable forever, unref, and maxRetryTime options for long-running or backgrounded retry loops

Common Use Cases

  • Retrying flaky DNS resolution or outbound network requests with exponential backoff
  • Wrapping database or API client calls that occasionally fail due to transient errors
  • Building higher-level retry and resilience libraries on top of a stable timeout primitive
  • Implementing bounded retry loops inside CLI tools and background workers

Under The Hood

Architecture retry’s architecture is two small CommonJS modules: lib/retry.js exposes the module-level factory functions (operation, timeouts, createTimeout, wrap), and lib/retry_operation.js defines the RetryOperation constructor that holds all mutable state for one retry sequence (attempt count, accumulated errors, timers). index.js simply re-exports lib/retry.js as the package entry point. retry.timeouts() precomputes an array of millisecond delays from options (retries, factor, minTimeout, maxTimeout, randomize), sorts it ascending, and retry.operation() wraps that array in a RetryOperation. Callers drive the lifecycle themselves: attempt(fn) runs the function once, and calling operation.retry(err) inside the callback shifts the next timeout off the internal array and reschedules fn via setTimeout, or returns false once the array is exhausted or maxRetryTime elapses — at which point mainError() picks the most-frequently-seen error out of the accumulated list. There is no event emitter, promise wrapper, or external I/O; the whole thing is a callback-driven state machine over a plain array of timeouts.

Tech Stack retry has zero runtime dependencies (package.json lists none) and targets Node >=4, reflecting its 2011 origin and long-term stability. It’s plain ES5 CommonJS — no TypeScript, no build step, no bundler — with main pointing straight at index.js. Dev tooling is equally minimal: istanbul for coverage and tape for its integration-style tests, run via a Makefile-driven npm test. There are no peerDependencies, no ESM entry point, and no bundled type definitions (consumers rely on the separate @types/retry package from DefinitelyTyped).

Code Quality The test suite under test/integration/ (test-timeouts.js, test-retry-operation.js, test-retry-wrap.js, test-forever.js) uses tape’s assert-based style and covers backoff math, forever mode, and the wrap() helper reasonably thoroughly for a project this size. Error handling is deliberately permissive — retry() returns false rather than throwing once retries are exhausted, pushing the exhausted-retry decision to the caller via mainError(). Naming is short and consistent (attempt, retry, errors, mainError), but the code predates modern JS idioms: var-based scoping throughout, manual prototype assignment on RetryOperation, and no strict-mode pragma. Deprecated aliases (try/start) are kept for backward compatibility, slightly cluttering the public surface.

API Design The core API is small and memorable — retry.operation(options) plus operation.attempt(fn) and operation.retry(err) cover the common case in three lines, and the README’s DNS-resolution example demonstrates the pattern well. The API is callback-oriented and predates Promises/async-await, so promise-based usage (also shown in the README) requires manual wrapping — there’s no built-in promise or async/await variant. Options (retries, factor, minTimeout, maxTimeout, randomize, forever, unref, maxRetryTime) are well documented in the README’s table, and retry.wrap() offers a convenient way to retrofit retry logic onto an existing object’s methods without rewriting call sites.

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

Search