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

retry-pro

v1.2.0

Published

A production-grade, policy-driven TypeScript retry package.

Readme

retry-pro

A production-grade, policy-driven TypeScript retry library.

NPM Version License: MIT Tests

Overview

retry-pro provides a structured, composable approach to retrying failed async operations. Instead of re-implementing retry logic per call site, it gives you a Policy-First model that is predictable, cancellable, observability-ready, and safe for production at scale.

Key properties:

  • Zero runtime dependencies
  • Full TypeScript types with strict mode
  • Works in Node.js and browsers (separate browser bundle)
  • Tree-shakable ESM + CJS dual output

Installation

npm install retry-pro

Quick Start

import { retry } from 'retry-pro';

const result = await retry(async (signal) => {
  return await fetch('/api/data', { signal }).then(r => r.json());
}, { retries: 3 });

Always forward the signal argument into your async operation. This enables clean cancellation and hedging.


Built-in Policies

Policies bundle a retry count, delay strategy, and retry condition together. Pass a policy name instead of configuring everything manually.

| Policy | Retries | Strategy | Retries On | |---|---|---|---| | httpSafe | 3 | Exponential (1000ms base) | 5xx only | | networkOnly | 5 | Exponential (500ms base) | Network errors (ECONNRESET, ETIMEDOUT, etc.) | | aggressive | 10 | Fixed (100ms) | Everything | | safe | 2 | Exponential (2000ms base) | 5xx + network errors |

const data = await retry(fetchMyData, { policy: 'httpSafe' });

Register Custom Policies

import { registerPolicy, exponentialStrategy, retry } from 'retry-pro';

registerPolicy('databaseFailover', {
  retries: 3,
  strategy: exponentialStrategy(500, 2, 10000), // base 500ms, factor 2, max 10s
  shouldRetry: (err) => err.code === 'ECONNRESET'
});

await retry(queryDb, { policy: 'databaseFailover' });

Attempting to register a policy name that already exists will throw an error.


Delay Strategies

Strategies control how long to wait between attempts. All strategies cap at maxDelay (default: 30,000ms).

Exponential

import { exponentialStrategy } from 'retry-pro';
// delay = base * (factor ^ attempt), capped at maxDelay
exponentialStrategy(baseDelay: number, factor?: number, maxDelay?: number)
// e.g. exponentialStrategy(1000) → 1000, 2000, 4000, 8000...

Linear

import { linearStrategy } from 'retry-pro';
// delay = base + (increment * attempt)
linearStrategy(baseDelay: number, increment: number, maxDelay?: number)
// e.g. linearStrategy(200, 100) → 200, 300, 400...

Fixed

import { fixedStrategy } from 'retry-pro';
// delay = constant
fixedStrategy(delay: number, maxDelay?: number)
// e.g. fixedStrategy(500) → 500, 500, 500...

With Jitter

Wraps any strategy to add randomized variance, preventing thundering herd problems.

import { withJitter, exponentialStrategy } from 'retry-pro';
// delay = baseDelay + (baseDelay * jitterFactor * random)
withJitter(strategy, jitterFactor?: number, maxDelay?: number)
// e.g. withJitter(exponentialStrategy(1000), 0.2)

Core Options Reference

await retry(fn, {
  retries: 3,                    // Max retry attempts
  policy: 'httpSafe',            // Built-in or custom policy name
  strategy: fixedStrategy(500),  // Delay strategy (overrides policy's)
  shouldRetry: (err) => true,    // Condition to retry (overrides policy's)
  signal: controller.signal,     // AbortSignal for cancellation
  attemptTimeout: 5000,          // Hard timeout per attempt (ms)
  fallback: () => defaultValue,  // Value or async fn when all retries fail
  adaptive: true,                // Adaptive delay based on error type
  priority: 'high',              // 'low' | 'normal' | 'high'
  onRetry: async (ctx) => {},    // Hook before each retry
  onSuccess: async (ctx) => {},  // Hook on success
  onFailure: async (ctx) => {},  // Hook on final failure
});

Lifecycle Hooks

All hooks receive a rich context object.

await retry(fn, {
  retries: 3,
  onRetry: async ({ attempt, strategyDelayMs, totalElapsedMs, error }) => {
    console.warn(`Attempt ${attempt} failed. Retrying in ${strategyDelayMs}ms. Elapsed: ${totalElapsedMs}ms`);
  },
  onSuccess: async ({ attempt, totalElapsedMs, result }) => {
    console.log(`Succeeded on attempt ${attempt}`);
  },
  onFailure: async ({ attempt, totalElapsedMs, error }) => {
    console.error(`All retries exhausted after ${attempt} attempts`, error);
  }
});

Cancellation (AbortSignal)

Pass an AbortSignal to cancel the entire retry loop, including any active delay intervals.

const controller = new AbortController();

// Cancel after 10 seconds
setTimeout(() => controller.abort(), 10000);

try {
  await retry(fn, { signal: controller.signal, retries: 10 });
} catch (err) {
  if (err instanceof AbortError) {
    console.log('Operation was cancelled');
  }
}

Fallbacks & Attempt Timeouts

const result = await retry(fetchPrices, {
  retries: 2,
  attemptTimeout: 5000,                          // Reject any attempt taking > 5s
  fallback: () => ({ cached: true, price: 50 }) // Returned after all retries fail
});

Adaptive Delay

When adaptive: true, the delay strategy is determined automatically by inspecting the error:

| Error Type | Strategy | |---|---| | Rate limited (429) | Slow exponential (1000 * 3^attempt) or uses Retry-After header | | Network error (ECONNRESET, ETIMEDOUT, etc.) | Fast incremental (100 * (attempt + 1)) | | Everything else | Standard exponential (1000 * 2^attempt) |

await retry(apiCall, { adaptive: true });

Circuit Breaker

Prevents cascading failures by stopping requests to a known-failing service. Uses a rolling failure window.

import { retry, CircuitBreaker } from 'retry-pro';

const circuit = new CircuitBreaker({
  failureThreshold: 3,   // Open after 3 failures
  resetTimeout: 30000,   // Try again after 30s (HALF_OPEN)
  successThreshold: 2,   // Require 2 successes to fully close
  window: 60000          // Only count failures within last 60s
});

await retry(fetchUsers, { circuitBreaker: circuit });

States: CLOSEDOPENHALF_OPENCLOSED

  • The circuit only opens on final failures (after all retries), not intermediate ones.
  • You can also pass raw CircuitBreakerOptions and an instance will be created for you.
await retry(fn, {
  circuitBreaker: { failureThreshold: 5, resetTimeout: 30000 }
});

Retry Budget

Prevents retry storms by capping the total retries allowed across multiple calls in a rolling time window.

import { retry, RetryBudget } from 'retry-pro';

const budget = new RetryBudget({ maxRetries: 50, window: 60000 });

// All retry() calls sharing this budget instance will collectively
// be limited to 50 retries per 60 seconds.
await retry(queryCluster, { retryBudget: budget });

Methods available on RetryBudget:

budget.canRetry()    // → boolean
budget.consume()     // Atomically checks and decrements; returns boolean
budget.getStats()    // → { remaining, resetIn, used }
budget.reset()       // Manually reset the window

You can also supply options directly and an instance will be created internally:

await retry(fn, { retryBudget: { maxRetries: 50, window: 60000 } });

Request Hedging

Launches a duplicate request if the original hasn't responded within a threshold, resolving with whichever completes first. Losers are automatically aborted.

await retry(async (signal) => fetch('/data', { signal }), {
  hedging: {
    enabled: true,
    delay: 200,          // Launch a hedge after 200ms
    maxHedges: 1,        // Max number of duplicate requests (default: 1)
    concurrencyLimit: 20 // Global limit on simultaneous hedge requests
  }
});

⚠️ Only use hedging on idempotent operations. It increases load on downstream services.


Priority System

Automatically tunes retry count and delays without manual configuration.

| Priority | Retries | Delay | |---|---|---| | high | +2 | ×0.5 (50% faster) | | normal | unchanged | unchanged | | low | −1 (min 0) | ×1.5 (50% slower) |

await retry(criticalAction, { priority: 'high' });
await retry(backgroundTask, { priority: 'low' });

Error Types

import { AbortError, RetryError, CircuitOpenError } from 'retry-pro';

try {
  await retry(fn, { retries: 3 });
} catch (err) {
  if (err instanceof AbortError) { /* cancelled */ }
  if (err instanceof RetryError) {
    console.log(err.attempts);   // How many attempts were made
    console.log(err.lastError);  // The underlying error
  }
  if (err instanceof CircuitOpenError) { /* circuit is open */ }
}

OpenTelemetry Integration

Import from the retry-pro/otel sub-path to avoid bundling OTEL into your main bundle.

import { withTracing } from 'retry-pro/otel';

const data = await withTracing(fetchDatabase, retryOptions, yourOtelTracer);

Emits span events on each retry attempt and sets the span status to OK or ERROR on completion.


Production Example

import { retry, CircuitBreaker, RetryBudget } from 'retry-pro';

const circuit = new CircuitBreaker({ failureThreshold: 5, resetTimeout: 30000 });
const budget = new RetryBudget({ maxRetries: 50, window: 60000 });

try {
  const data = await retry(async (signal) => axios.get('/api', { signal }), {
    policy: 'httpSafe',
    priority: 'high',
    attemptTimeout: 5000,
    adaptive: true,
    hedging: { enabled: true, delay: 250 },
    circuitBreaker: circuit,
    retryBudget: budget,
    onRetry: ({ attempt, strategyDelayMs }) => {
      logger.warn(`Retry ${attempt}, waiting ${strategyDelayMs}ms`);
    }
  });
} catch (err) {
  if (err instanceof CircuitOpenError) {
    // Serve from cache or degrade gracefully
  }
}

Safety: Idempotency

⚠️ Only retry idempotent operations. If a POST request creates a record but times out before returning a response, retrying it will create a duplicate. Use retry only for safe operations (reads, or writes protected by idempotency keys).


License

MIT