npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@verifyhash/retry

v0.1.0

Published

Zero-dependency async retry for Node.js with exponential backoff and full jitter (AWS-style), AbortSignal support that cancels even mid-sleep, shouldRetry/onRetry hooks, and injectable sleep + rand so tests are exact and instant without fake-timer librari

Readme

@verifyhash/retry

Zero-dependency async retry with exponential backoff + jitter for Node.js, typed from birth (hand-written index.d.ts ships with the package). The backoff model is the AWS-recommended one: the un-jittered delay is min(minDelay * factor^(attempt-1), maxDelay), and the default 'full' jitter multiplies it by a uniform random in [0, 1) — so 10 clients that all fail at once do NOT hammer the server again in lockstep.

Its distinguishing feature is determinism without fake timers: the backoff math lives in a pure, exported computeDelay(attempt, opts, rand) (the random source is an argument), and retry() accepts an injectable sleep(ms, signal). Your tests pass a stub sleep that records the requested delays and resolves immediately, plus a fixed rand — every backoff vector is exact and the whole suite runs in milliseconds. This library's own 70-check suite does exactly that (node test/index.test.js finishes in under 100 ms with zero dev dependencies).

Who it's for: Node.js developers wrapping flaky calls (HTTP, DNS, databases, rate-limited APIs) who want cancellation via a standard AbortSignal — including mid-sleep, where naive implementations keep a dead timer pending — and who want to unit-test their retry policy exactly instead of sprinkling jest.useFakeTimers() around.

Honest limits: JavaScript cannot interrupt a promise that is already running, so aborting does NOT cancel an in-flight fn() call — the abort takes effect at the next boundary (mid-sleep or before the next attempt). If you need the attempt itself cancelled, pass the same signal into fn (e.g. to fetch). There is also no retry budget/deadline option; compose one with shouldRetry if you need it.

Install

npm install @verifyhash/retry

Example

const { retry, computeDelay } = require('@verifyhash/retry');

// Retry a flaky call up to 5 times: 100ms, 200ms, 400ms, 800ms base delays
// (each multiplied by Math.random() under the default 'full' jitter).
const controller = new AbortController();
const data = await retry(
  (attempt) => fetchThing({ signal: controller.signal, attempt }),
  {
    attempts: 5,
    minDelay: 100,
    maxDelay: 2000,               // caps the un-jittered delay
    signal: controller.signal,    // abort() rejects promptly, even mid-sleep
    shouldRetry: (err) => err.code !== 'FATAL', // give up on fatal errors
    onRetry: (err, attempt, delay) =>
      console.warn(`attempt ${attempt} failed (${err.message}), retrying in ${delay}ms`),
  }
);

// The pure calculator, pinned exactly in tests via an injected rand:
computeDelay(3, { minDelay: 100, factor: 2, jitter: 'none' });      // 400
computeDelay(3, { minDelay: 100, factor: 2 }, () => 0.5);           // 200 (full jitter, rand=0.5)
computeDelay(10, { minDelay: 100, maxDelay: 30000, jitter: 'none' }); // 30000 (clamped from 51200)

Deterministic testing pattern (what this library's own suite does — no fake timers, no real waiting):

const { retry } = require('@verifyhash/retry');

const delays = [];
const stubSleep = (ms) => { delays.push(ms); return Promise.resolve(); };

let n = 0;
const v = await retry(() => { if (++n < 3) throw new Error('boom'); return 42; },
  { attempts: 5, minDelay: 100, jitter: 'none', sleep: stubSleep });
// v === 42, delays === [100, 200] — exact, instant.

API

retry(fn, options?) → Promise

Calls fn(attempt) (attempt is 1-based) until it resolves. Resolves with fn's value. Rejects with:

  • the last error, once attempts are exhausted;
  • the error itself, immediately, when shouldRetry(error, attempt) returns false (no sleep happens);
  • an AbortError-shaped error (name: 'AbortError', code: 'ABORT_ERR') when options.signal aborts — before the first attempt, mid-sleep (the default sleep clears its pending timer), or between attempts. No further attempts run after an abort. If the signal was aborted with an Error reason (Node's default reason is a DOMException named AbortError), that reason is the rejection, matching fetch() semantics.

Options (all optional):

| option | default | meaning | | ------------- | ------------- | ------- | | attempts | 3 | total tries including the first (integer ≥ 1) | | minDelay | 100 | base delay in ms before the first retry | | maxDelay | 30000 | cap on the un-jittered delay (must be ≥ minDelay) | | factor | 2 | exponential base (≥ 1; 1 = flat backoff) | | jitter | 'full' | 'full' = delay × rand(); 'none' = exact | | signal | — | AbortSignal; see abort semantics above | | shouldRetry | retry all | (error, attempt) => boolean \| Promise<boolean> | | onRetry | — | (error, attempt, delay) hook, fired before each sleep | | sleep | real timer | injectable (ms, signal) => Promise<void> | | rand | Math.random | injectable random source in [0, 1) for 'full' jitter |

Invalid options (e.g. attempts: 0, jitter: 'half', maxDelay < minDelay) reject with RangeError/TypeError. Because fn cannot be interrupted, a value fn resolves AFTER an abort is still delivered.

computeDelay(attempt, options?, rand?) → number

Pure backoff calculator used by retry and exported for exact tests and custom schedulers: min(minDelay * factor^(attempt-1), maxDelay), multiplied by rand() when jitter is 'full'. attempt is the 1-based attempt that just failed (integer ≥ 1, else RangeError). Accepts the same minDelay/maxDelay/factor/jitter options as retry and returns milliseconds (fractional under 'full' jitter — e.g. rand = () => 0.999 at attempt 1 gives 99.9).

TypeScript users also get the exported types RetryOptions, BackoffOptions, Jitter, SleepFn, and RandomSource.

How to test

cd retry
node test/index.test.js   # 70 checks, zero dependencies, < 100 ms

The suite pins golden vectors for computeDelay (growth, clamp, both jitter modes with fixed rand), success-after-N-failures, exhaustion, shouldRetry short-circuit (sync and async), the onRetry call log, and three abort paths (pre-start, mid-sleep via a stub sleep, and mid-sleep through the real default setTimeout sleep — aborted synchronously, so even that test never actually waits).

License

MIT