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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@selentia/async-retry

v1.0.0

Published

Zero-runtime-deps async retry engine with backoff, full jitter, and Retry-After support (Node + Browser).

Readme

@selentia/async-retry

A zero-dependency retry policy library for Node.js and browsers.
It supports exponential backoff, Retry-After handling, AbortSignal integration, full jitter,
and an overall max elapsed time limit via maxElapsedMs. Runs on Node.js ≥18 and modern browsers.

Used in production by Pastellink, a Discord bot trusted by 2,500+ servers.

📄 Other languages:


Table of Contents


Install

npm i @selentia/async-retry

Quick Start

retry

import { retry } from '@selentia/async-retry';

const data = await retry(async ({ attempt }) => {
  const res = await fetch('/api/data');
  if (!res.ok) throw new Error(`HTTP ${res.status} (attempt=${attempt})`);
  return res.json();
});

createRetry

createRetry() applies default options and supports per-call overrides.

import { createRetry } from '@selentia/async-retry';

const retryFetch = createRetry({
  maxAttempts: 5,
  baseMs: 200,
  capMs: 4000,
  jitter: 'full',
});

const json = await retryFetch(
  async () => {
    const r = await fetch('/api/data');
    if (!r.ok) throw new Error(`HTTP ${r.status}`);
    return r.json();
  },
  {
    // per-call overrides (shallow merge)
    maxElapsedMs: 10_000,
  },
);

API

retry(task, options?) → Promise<T>

task receives a RetryContext:

await retry(async (ctx) => {
  ctx.attempt;      // 1..maxAttempts
  ctx.maxAttempts;  // max attempts
  ctx.startedAt;    // epoch ms when retry() started
  ctx.elapsedMs;    // elapsed ms since startedAt (int)
  ctx.signal;       // AbortSignal (if provided)
  return 'ok';
});

createRetry(defaultOptions) → (task, overrides?) => Promise<T>

Returns a retry-compatible function that applies defaultOptions first. Overrides are merged shallowly ({ ...defaultOptions, ...overrides }), so nested objects are not deep-merged.


Options

The following defaults are applied:

| Option | Type | Default | Description | |--------|------|---------|-------------| | maxAttempts | number | 3 | Total attempts including the first call. Must be an integer ≥ 1. | | baseMs | number | 200 | Base backoff (ms). Must be finite ≥ 0. | | capMs | number | 2000 | Backoff cap (ms). Must be finite ≥ 0. | | factor | number | 2 | Exponential factor. Must be finite > 0. | | jitter | 'full' \| 'none' | 'full' | Full jitter randomizes delay in [0, backoff). | | rng | () => number | Math.random | Random source for jitter. Non-finite results are treated as 0. | | signal | AbortSignal | undefined | Aborts the entire retry loop (including sleep). | | maxElapsedMs | number | undefined | Overall time budget (ms), checked before each attempt and before sleeping. | | shouldRetry | (err, ctx) => boolean \| Promise<boolean> | defaultShouldRetry | Determines whether the error is retriable. | | onRetry | (event) => void | undefined | Hook called immediately before sleeping. | | wrapError | boolean | false | If true, wraps exhausted/non-retriable failures into RetryExhaustedError. | | respectRetryAfter | boolean | true | If true, respects Retry-After for 429. | | retryAfterHeaderName | string | 'retry-after' | Header name (case-insensitive). Whitespace is trimmed; empty falls back to default. | | retryAfterBodyUnit | false \| 'seconds' \| 'milliseconds' | false | If enabled, reads retry_after from the response body when the header is missing. |


Retry-After semantics

When status === 429 and respectRetryAfter === true:

  1. The Retry-After header is checked first (case-insensitive key match).
  • Numeric values are treated as seconds.
  • HTTP-date values are parsed and converted to max(0, date - now) in ms.
  1. If there is no usable header and retryAfterBodyUnit !== false, a body value is used:
  • Reads retry_after in the following order: err.response.data.retry_after, err.rawError.retry_after, err.data.retry_after
  • String/number values are parsed; the unit is controlled by retryAfterBodyUnit.

If neither yields a usable delay, it falls back to regular exponential backoff.

Within onRetry(event), event.reason will be:

  • 'retry-after' when Retry-After is used
  • 'backoff' when exponential backoff is used

Abort & Timeout semantics

  • If signal is already aborted before an attempt, retry() throws AbortError and does not call the task.
  • If aborted during sleep, sleep is interrupted and AbortError is thrown.
  • If the task throws an “abort-like” error (name === 'AbortError' or code === 'ABORT_ERR' / code === 'ERR_CANCELED'), it is propagated immediately (no retries).
  • maxElapsedMs is enforced:
    • before each attempt
    • and before sleeping (so a long delay cannot exceed the budget)

Errors

These errors can be handled via instanceof.

| Error | When it occurs | |------|----------------| | AbortError | The retry loop is aborted (before an attempt or during sleep). | | RetryTimeoutError | The maxElapsedMs budget is exceeded (before an attempt or before sleeping). | | RetryExhaustedError | wrapError=true and the loop ends due to exhaustion or a non-retriable decision (the original error is available as cause). |

Example:

import { retry } from '@selentia/async-retry';
import { AbortError, RetryTimeoutError, RetryExhaustedError } from '@selentia/async-retry/errors';

try {
  await retry(async () => {
    // ...
  }, { maxElapsedMs: 2000, wrapError: true });
} catch (err) {
  if (err instanceof AbortError) {
    // aborted by signal
  } else if (err instanceof RetryTimeoutError) {
    // budget exceeded
  } else if (err instanceof RetryExhaustedError) {
    // exhausted or non-retriable (the original error is available as `err.cause`)
  }
}

Guarantees

  • Attempts are 1-indexed: the first call is attempt = 1.
  • maxAttempts is never exceeded.
  • onRetry() is called only when a retry will actually happen, and it is called before sleeping.
  • When Retry-After is used, jitter is not applied; the delay is taken as-is (normalized to a non-negative integer ms).
  • All delays are normalized to integer milliseconds (>= 0).

License

MIT