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

@onelifepolymath/retryflow

v0.1.1

Published

Elegant async retry with advanced backoff strategies for resilient operations

Readme

retryflow

Elegant async retry with advanced backoff strategies for resilient operations.

Network requests, database connections, and external API calls fail. retryflow makes them resilient with intelligent retry logic, multiple backoff strategies, and jitter — all in a zero-dependency TypeScript package.

Features

| Feature | Description | |---------|-------------| | Multiple Backoff Strategies | Exponential, linear, and fixed delay strategies | | Jitter Modes | Full jitter and equal jitter to prevent thundering herd | | Conditional Retry | shouldRetry callback to filter which errors trigger retries | | Success Hooks | onSuccess for recovery observability after failures | | Retry Hooks | onRetry for logging, metrics, and side effects | | Retry Policies | Pre-built configurations (exponential, linear, fixedDelay, aggressive) | | Respect Retry-After | respectRetryAfter uses error's retryAfter property as the delay | | Timeout Support | Per-attempt timeout and total elapsed time budget | | Cancellation | AbortSignal support for external cancellation; signal forwarded to your function | | Fallback on Exhaustion | Static value or fallback function when all retries fail | | Fluent Builder API | RetryPipeline — chainable builder for complex configurations | | Retry Events | Discriminated union events for every lifecycle point (onEvent) | | Context Propagation | Pass typed context through all hooks and callbacks | | Pre-configured Retry | withRetryDefaults factory for reusable retry instances | | Function Wrapping | wrapRetry decorator preserves type signatures | | Type Safe | Full TypeScript generics — inferred return types | | Zero Dependencies | No runtime dependencies, minimal attack surface | | Dual Module | CJS (require) and ESM (import) |

Installation

npm install @onelifepolymath/retryflow

Quick Start

import { retryflow } from 'retryflow';

const data = await retryflow(async (attempt) => {
  const res = await fetch('https://api.example.com/data');
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json();
}, {
  maxAttempts: 3,
  baseDelay: 1000,
  backoffType: 'exponential',
  jitter: 'full',
});

Usage

CommonJS

const { retryflow } = require('retryflow');

async function main() {
  const result = await retryflow(async (attempt) => {
    const res = await fetch('https://api.example.com/data');
    if (!res.ok) throw new Error(`Request failed: ${res.status}`);
    return res.json();
  }, {
    maxAttempts: 3,
    baseDelay: 500,
  });
  console.log(result);
}

Conditional Retry with shouldRetry

import { retryflow } from 'retryflow';

const data = await retryflow(async (attempt) => {
  const res = await fetch('https://api.example.com/data');
  if (!res.ok) {
    const error = new Error(`HTTP ${res.status}`);
    (error as any).status = res.status;
    throw error;
  }
  return res.json();
}, {
  shouldRetry: (error, attempt) => {
    // Don't retry 4xx errors, retry 5xx and network errors
    return (error as any).status >= 500 || (error as any).status === 429;
  },
});

Logging with onRetry

import { retryflow } from 'retryflow';

const data = await retryflow(async (attempt) => {
  // ... flaky operation
}, {
  onRetry: (error, attempt) => {
    console.warn(`Attempt ${attempt} failed: ${error.message}. Retrying...`);
  },
});

Observing Recovery with onSuccess

import { retryflow } from 'retryflow';

const data = await retryflow(async (attempt) => {
  // ... flaky operation
}, {
  onSuccess: (result, attempt) => {
    console.log(`Recovered on attempt ${attempt}`);
  },
});

The onSuccess callback fires only when the function succeeds after at least one failure. It does not fire on first-attempt success.

Rate-Limit Aware Retry with respectRetryAfter

import { retryflow } from 'retryflow';

const data = await retryflow(async (attempt) => {
  const res = await fetch('https://api.example.com/data');
  if (res.status === 429) {
    const error = new Error('Rate limited');
    // Set retryAfter (in ms) to override the calculated delay
    (error as any).retryAfter = 5000;
    throw error;
  }
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json();
}, {
  respectRetryAfter: true,
});

When respectRetryAfter is true and the thrown error has a numeric retryAfter property, that value is used as the delay instead of the calculated backoff.

Custom Delay with delayFn

import { retryflow } from 'retryflow';

const data = await retryflow(async (attempt) => {
  // ... flaky operation
}, {
  delayFn: (attempt, error) => {
    // Exponential: 2^attempt * 1000
    return Math.pow(2, attempt) * 1000;
  },
});

When delayFn is provided, it completely replaces the built-in backoff calculation. Use it for custom delay algorithms, circuit-breaker-aware delays, or dynamic delays based on error properties.

Using Retry Policies

import { retryflow, policies } from 'retryflow';

// Use pre-built policies with optional overrides
const data = await retryflow(fn, policies.exponential({ maxAttempts: 5 }));
// or: policies.linear(), policies.fixedDelay(), policies.aggressive()

Fallback on Exhaustion

When all retry attempts fail, you can provide a fallback value or function instead of throwing:

import { retryflow } from 'retryflow';

// Static fallback value
const data = await retryflow(flakyFn, {
  fallback: 'cached_default',
});

// Fallback function — receives attempt count and all errors
const data = await retryflow(flakyFn, {
  fallback: async (attempt, errors) => {
    console.warn(`Failed ${errors.length} times, using cache`);
    return await cache.get('fallback');
  },
});

Retry Events (onEvent)

A unified event callback for deep observability into the retry lifecycle:

import { retryflow } from 'retryflow';

const data = await retryflow(fn, {
  onEvent: (event) => {
    switch (event.type) {
      case 'attempt':
        console.log(`Attempt ${event.attempt} starting...`);
        break;
      case 'success':
        console.log(`Attempt ${event.attempt} succeeded (${event.duration}ms)`);
        break;
      case 'error':
        console.log(`Attempt ${event.attempt} failed: ${event.error.message}`);
        break;
      case 'retry':
        console.log(`Retrying attempt ${event.attempt} after ${event.delay}ms`);
        break;
      case 'abort':
        console.log('Operation aborted');
        break;
      case 'failure':
        console.log(`Failed (${event.reason}): ${event.error.message}`);
        break;
      case 'fallback':
        console.log(`Using fallback after ${event.errors.length} errors`);
        break;
      case 'timeout':
        console.log(`Attempt timed out after ${event.timeoutMs}ms`);
        break;
    }
  },
});

Event types:

| Event | Shape | When it fires | |-------|-------|---------------| | attempt | { type, attempt, signal? } | Before each attempt | | success | { type, attempt, result, duration } | On successful attempt | | error | { type, attempt, error, duration } | On failed attempt | | retry | { type, attempt, error, delay } | Before retry delay | | abort | { type, attempt, errors } | On abort signal | | failure | { type, attempt, error, errors, reason } | On final failure (reason: exhausted, non-retryable, aborted, timeout) | | fallback | { type, attempt, errors } | When fallback is used | | timeout | { type, attempt, timeoutMs } | When per-attempt timeout fires |

Fluent Builder with RetryPipeline

For complex configurations, use the fluent builder API:

import { RetryPipeline } from 'retryflow';

const result = await RetryPipeline
  .configure({ maxAttempts: 3 })
  .baseDelay(500)
  .backoff('exponential')
  .jitter('full')
  .timeout(5000)
  .maxElapsed(15000)
  .withFallback('degraded')
  .withContext({ requestId: 'abc' })
  .onRetry((error, attempt, ctx) => {
    logger.warn(`[${ctx.requestId}] Attempt ${attempt}: ${error.message}`);
  })
  .run(async (attempt, signal) => {
    const res = await fetch(url, { signal });
    return res.json();
  });

Using AbortSignal

The signal is forwarded to your function as a second argument, so you can cancel the underlying operation itself:

import { retryflow } from 'retryflow';

const controller = new AbortController();
setTimeout(() => controller.abort(), 5000);

const data = await retryflow(async (attempt, signal) => {
  const res = await fetch(url, { signal });
  return res.json();
}, {
  signal: controller.signal,
});

Configuration

RetryOptions

| Option | Type | Default | Description | |--------|------|---------|-------------| | maxAttempts | number | 3 | Total attempts including the initial call | | retries | number | — | Number of retries (overrides maxAttempts if set, using maxAttempts = retries + 1) | | baseDelay | number | 1000 | Base delay in milliseconds | | backoffType | 'exponential' \| 'linear' \| 'fixed' | 'exponential' | Backoff strategy | | maxDelay | number | Infinity | Maximum delay cap in milliseconds | | jitter | 'full' \| 'equal' \| 'none' | 'none' | Jitter mode | | shouldRetry | (error, attempt) => boolean \| Promise<boolean> | () => true | Called after each failure. Return false to stop retrying | | onRetry | (error, attempt) => void \| Promise<void> | — | Called before each retry attempt | | onSuccess | (result, attempt) => void \| Promise<void> | — | Called when a retry succeeds after at least one failure | | delayFn | (attempt, error) => number \| Promise<number> | — | Replaces the built-in delay calculation with a custom function | | maxElapsedMs | number | Infinity | Maximum total elapsed time in milliseconds | | attemptTimeoutMs | number | Infinity | Timeout per attempt in milliseconds | | signal | AbortSignal | — | AbortSignal for external cancellation | | respectRetryAfter | boolean | false | When true, uses error's numeric retryAfter property (ms) as the delay | | fallback | T \| (attempt, errors) => T \| Promise<T> | — | Static value or function returning a fallback result when all retries are exhausted | | onEvent | (event: RetryEvent) => void \| Promise<void> | — | Unified callback for all retry lifecycle events (attempt, success, error, retry, abort, failure, fallback, timeout) |

Backoff Strategies

Exponential (default)

delay = min(maxDelay, baseDelay * 2^attempt)

Doubles the delay after each attempt. Use when you want to quickly back off from a failing service.

Linear

delay = min(maxDelay, baseDelay * (attempt + 1))

Increases delay linearly. Use for rate-limited APIs or predictable backoff.

Fixed

delay = baseDelay

Constant delay between all attempts. Use for polling or steady-interval retries.

Jitter Modes

Full Jitter

delay = Math.random() * calculatedDelay

Random delay between 0 and the calculated backoff. Best for preventing thundering herd problems. Recommended by AWS.

Equal Jitter

delay = calculatedDelay * (0.5 + Math.random() * 0.5)

Keeps delay between 50-100% of the calculated value. Good balance of randomness and minimum wait time.

None

delay = calculatedDelay

No randomness. Use for deterministic retry intervals where jitter is not needed.

Error Handling

When all retry attempts are exhausted (or retry is stopped) and no fallback is provided, retryflow throws a RetryError:

import { retryflow, RetryError, isRetryError } from 'retryflow';

try {
  const result = await retryflow(flakyFn, { maxAttempts: 3 });
} catch (error) {
  if (isRetryError(error)) {
    console.error(`Failed after ${error.attempts} attempts`);
    console.error('All errors:', error.retryErrors);
    console.error('Last error cause:', error.cause);
  }
}

RetryError properties:

| Property | Type | Description | |----------|------|-------------| | .message | string | Reason for stopping ('All retry attempts failed', 'Non-retryable error: ...', etc.) | | .attempts | number | Total attempts made | | .retryErrors | Error[] | Every error encountered during retries | | .cause | Error \| undefined | The last error that occurred (using native Error.cause) |

API Reference

retryflow<T>(fn, options?)

Type parameters:

  • T — Return type of the function (inferred automatically)

Parameters:

  • fn(attempt: number, signal?: AbortSignal) => Promise<T> — The async function to retry. Receives the 1-indexed attempt number and optionally the AbortSignal from options.
  • optionsRetryOptions (optional)

Returns: Promise<T> — Resolves with the function's return value.

Throws: RetryError — When all attempts fail or a non-retryable error occurs.

wrapRetry<T>(fn, options?)

Wraps an async function with retry logic, returning a new function with the same signature.

Type parameters:

  • T — Tuple of argument types (inferred)

Parameters:

  • fn(...args: T) => Promise<U> — The async function to wrap
  • optionsRetryOptions (optional)

Returns: (...args: T) => Promise<U> — A new function with retry built in.

withRetryDefaults(options)

Creates a pre-configured retry function with baked-in default options.

Parameters:

  • optionsRetryOptions — Default options for all retries

Returns: <T>(fn, overrides?) => Promise<T> — A retry function that merges defaults with per-call overrides.

const query = withRetryDefaults({ maxAttempts: 3, baseDelay: 500 });
const users = await query(() => db.findUsers());
const posts = await query(() => db.findPosts(), { maxAttempts: 5 }); // override

RetryPipeline

A fluent builder API for configuring and running retry operations with context propagation.

Static methods:

  • RetryPipeline.configure(options?) — Creates a new pipeline instance

Builder methods (chainable):

| Method | Description | |--------|-------------| | .configure(options) | Merge additional options | | .retries(n) | Set max attempts | | .baseDelay(ms) | Set base delay | | .backoff(type) | Set backoff strategy | | .jitter(type) | Set jitter mode | | .maxDelay(ms) | Cap the delay | | .timeout(ms) | Per-attempt timeout | | .maxElapsed(ms) | Total elapsed time budget | | .signal(AbortSignal) | Attach abort signal | | .respectRetryAfter(bool) | Enable/disable retry-after | | .delayFn(fn) | Custom delay function | | .withFallback(value \| fn) | Fallback value or function | | .withContext(ctx) | Attach typed context | | .shouldRetry(fn) | Conditional retry with context | | .onRetry(fn) | Retry hook with context | | .onSuccess(fn) | Success hook with context | | .onFallback(fn) | Fallback hook with context | | .onEvent(fn) | Unified event hook with context | | .clone() | Create independent copy |

Terminal method:

  • pipeline.run<T>(fn): Promise<T> — Executes the operation with the configured options. Context-aware hooks receive (error, attempt, ctx).

Type parameters:

  • C — Context type (inferred from .withContext())
const pipeline = RetryPipeline
  .configure({ baseDelay: 200 })
  .jitter('full')
  .withContext({ region: 'us-east' })
  .onRetry((err, attempt, ctx) => {
    console.error(`[${ctx.region}] attempt ${attempt} failed`);
  });

// Run with per-call overrides
const result = await pipeline
  .clone()
  .retries(5)
  .withFallback('default')
  .run(myAsyncFn);

Examples

Database query with retry

import { retryflow } from 'retryflow';
import { query } from './db';

const users = await retryflow(
  () => query('SELECT * FROM users WHERE active = ?', [true]),
  { maxAttempts: 3, baseDelay: 200, backoffType: 'exponential' },
);

API call with per-attempt timeout

import { retryflow } from 'retryflow';

const data = await retryflow(async (attempt) => {
  const res = await fetch('https://api.example.com/data');
  return res.json();
}, {
  maxAttempts: 3,
  attemptTimeoutMs: 5000,
  maxElapsedMs: 15000,
});

Using retries option (alternative to maxAttempts)

// These are equivalent:
retryflow(fn, { retries: 2 });        // 1 initial + 2 retries = 3 total
retryflow(fn, { maxAttempts: 3 });    // 3 total attempts

wrapRetry

Wraps any async function with retry logic, preserving its type signature.

import { wrapRetry } from 'retryflow';

const queryWithRetry = wrapRetry(
  async (id: number) => db.query('SELECT * FROM users WHERE id = ?', [id]),
  { maxAttempts: 3, baseDelay: 200 },
);

// Type-safe: id is inferred as number, return type is inferred
const user = await queryWithRetry(42);

Default Import

import retryflow from 'retryflow';

// Same as named import
await retryflow(async () => { /* ... */ });

Advanced: calculateDelay

For custom retry logic, you can use the delay calculator directly:

import { calculateDelay } from 'retryflow';

const delay = calculateDelay(0, {
  maxAttempts: 3,
  baseDelay: 1000,
  backoffType: 'exponential',
  maxDelay: 30000,
  jitter: 'full',
  maxElapsedMs: Infinity,
  attemptTimeoutMs: Infinity,
});

Exported API

| Export | Type | Description | |--------|------|-------------| | retryflow (named + default) | function | Main retry function | | wrapRetry | function | Decorator to wrap functions with retry | | withRetryDefaults | function | Factory for pre-configured retry instances | | RetryPipeline | class | Fluent builder API for retry with context | | policies | object | Pre-built retry policies (.exponential, .linear, .fixedDelay, .aggressive) | | RetryError | class | Error thrown on retry exhaustion | | isRetryError | function | Type guard for RetryError (error is RetryError) | | calculateDelay | function | Pure delay calculation utility | | RetryOptions | interface | Configuration options type | | BackoffType | type | 'exponential' \| 'linear' \| 'fixed' | | JitterType | type | 'full' \| 'equal' \| 'none' | | AsyncFn | type | (...args: T) => Promise<U> | | RetryFn | type | (attempt: number, signal?: AbortSignal) => Promise<T> | | FallbackFn | type | (attempt: number, errors: Error[]) => T \| Promise<T> | | PipelineOptions | interface | Options type for RetryPipeline | | RetryEvent | type | Discriminated union of lifecycle event objects | | RetryEventType | type | 'attempt' \| 'success' \| 'error' \| 'retry' \| 'abort' \| 'failure' \| 'fallback' \| 'timeout' |

License

MIT