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

@zap-studio/retry

v0.3.0

Published

Composable retry policies for resilient async operations.

Readme

@zap-studio/retry

Composable retry policy primitives for HTTP clients and async workflows.

Installation

npm install @zap-studio/retry

Usage

import { ExponentialBackoff } from "@zap-studio/retry/exponential-backoff";
import { FixedDelay } from "@zap-studio/retry/fixed-delay";
import { $fetch } from "@zap-studio/fetch";

const exponential = new ExponentialBackoff({
  maxAttempts: 5,
  baseDelayMs: 100,
  maxDelayMs: 2_000,
});

const data = await exponential.run(async () => {
  const response = await $fetch("https://api.example.com/users", {
    throwOnFetchError: true,
  });
  return await response.json();
});

Handling Errors

run(...) throws when retries are exhausted.

By default, policies extending BaseRetryPolicy throw RetryError on exhaustion and AbortError on cancellation.

import { AbortError, RetryError } from "@zap-studio/retry/errors";

try {
  const data = await exponential.run(async () => {
    const response = await $fetch("https://api.example.com/users", {
      throwOnFetchError: true,
    });
    return await response.json();
  });
  console.log(data);
} catch (error) {
  if (error instanceof RetryError) {
    console.error("Retries exhausted:", error.attempts);
    console.error("Last error:", error.lastError);
  } else if (error instanceof AbortError) {
    console.error("Retry aborted:", error.message);
  } else {
    throw error;
  }
}

To handle exhaustion without throwing, pass throwOnExhausted: false:

const result = await exponential.run(
  async () => {
    const response = await $fetch("https://api.example.com/users", {
      throwOnFetchError: true,
    });
    return await response.json();
  },
  { throwOnExhausted: false },
);

if (!result.ok) {
  console.error("Retries exhausted:", result.attempts);
  console.error("Last error:", result.error.lastError);
} else {
  console.log(result.value);
}

Default sleep

BaseRetryPolicy.run automatically applies a delay between retry attempts when no custom sleep function is provided in the options.

That default is the defaultSleep helper, exported from @zap-studio/retry/sleep.

By default, this delay mechanism relies on the native JavaScript setTimeout, meaning retries are scheduled using the standard event loop timing rather than any custom or blocking implementation.

Cancellation With AbortSignal

Use signal in run(...) options to stop retrying early.

const controller = new AbortController();

const promise = exponential.run(
  async () => {
    const response = await $fetch("https://api.example.com/users", {
      throwOnFetchError: true,
    });
    return await response.json();
  },
  { signal: controller.signal },
);

controller.abort(new Error("Request canceled"));

await promise;

In non-throw mode, abort is returned as { ok: false } with AbortError on result.error:

const controller = new AbortController();

const result = await exponential.run(
  async () => {
    const response = await $fetch("https://api.example.com/users", {
      throwOnFetchError: true,
    });
    return await response.json();
  },
  {
    signal: controller.signal,
    throwOnExhausted: false,
  },
);

if (!result.ok) {
  console.error("Retry stopped:", result.error);
}

Choosing The Right Policy

Use ExponentialBackoff for transient network instability and shared upstream services.

const unstableNetworkPolicy = new ExponentialBackoff({
  maxAttempts: 6,
  baseDelayMs: 100,
  maxDelayMs: 2_000,
});

Use FixedDelay for stable, predictable retry intervals in controlled environments.

const predictableIntervalPolicy = new FixedDelay({
  maxAttempts: 4,
  delayMs: 300,
});

Custom Policies

Extend BaseRetryPolicy when the built-in policies do not match your retry rules.

You implement next(...) only; the base class supplies onExhausted with a default RetryError and keeps the shared run(...) orchestration (override onExhausted when you need a different terminal error).

import { BaseRetryPolicy } from "@zap-studio/retry";
import type { RetryDecision, RetryDecisionInput } from "@zap-studio/retry/types";

class LinearBackoff extends BaseRetryPolicy {
  constructor(
    private readonly maxAttempts: number,
    private readonly stepMs: number,
  ) {
    super();
  }

  public next(input: RetryDecisionInput): RetryDecision {
    if (input.attempt >= this.maxAttempts) {
      return {
        shouldRetry: false,
        delayMs: 0,
        reason: "max-attempts-reached",
      };
    }

    return {
      shouldRetry: true,
      delayMs: input.attempt * this.stepMs,
      reason: "retry",
    };
  }
}

const policy = new LinearBackoff(5, 250);
const value = await policy.run(doWork);

RetryError

Use RetryError when an orchestrator exhausts retries and needs to surface final context.

import { RetryError } from "@zap-studio/retry/errors";

throw new RetryError("Retry policy exhausted all attempts.", {
  attempts: attempt,
  lastError: error,
  lastData: data,
});

Policies implement onExhausted(input) to return the terminal error used by the built-in runner.

ExponentialBackoff and FixedDelay inherit the default implementation from BaseRetryPolicy.

License

MIT