Zap Studio

Custom Policies

Create custom retry policies by extending BaseRetryPolicy or implementing the retry types directly.

Create a custom policy when the built-in FixedDelay and ExponentialBackoff policies do not match your retry rules.

Extend BaseRetryPolicy

Extending BaseRetryPolicy is the recommended path for most custom policies.

You implement next(...) only; the base class provides onExhausted with a default RetryError return, keeps the shared run(...) orchestration, and lets you override onExhausted when needed.

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(async () => {
  return await doWork();
});

Customize Exhaustion

Override onExhausted(...) when callers need a custom terminal error.

import { BaseRetryPolicy } from "@zap-studio/retry";
import { RetryError } from "@zap-studio/retry/error";
import type { RetryExhaustedInput } from "@zap-studio/retry/types";

class UpstreamRetryError extends RetryError {
  constructor(
    public readonly attempts: number,
    public readonly cause: unknown,
  ) {
    super("Upstream retries exhausted.", {
      attempts,
      lastError: cause,
    });
  }
}

class UpstreamPolicy extends BaseRetryPolicy {
  public next(input) {
    return {
      shouldRetry: input.attempt < 3,
      delayMs: 100,
      reason: input.attempt < 3 ? "retry" : "max-attempts-reached",
    };
  }

  public onExhausted(input: RetryExhaustedInput): UpstreamRetryError {
    return new UpstreamRetryError(input.attempts, input.error);
  }
}

run(...) will throw the value returned by onExhausted(...), unless throwOnExhausted is false.

Implement The Types Directly

Use the lower-level RetryPolicy type when you need to build your own orchestration layer instead of using BaseRetryPolicy.run(...).

import type { RetryPolicy } from "@zap-studio/retry/types";
import { RetryError } from "@zap-studio/retry/error";

const policy: RetryPolicy = {
  next: ({ attempt }) => ({
    shouldRetry: attempt < 3,
    delayMs: 100,
    reason: attempt < 3 ? "retry" : "max-attempts-reached",
  }),
  onExhausted: ({ attempts, error }) =>
    new RetryError("Retry policy exhausted all attempts.", {
      attempts,
      lastError: error,
    }),
};

When you implement RetryPolicy directly, you are responsible for the retry loop, delay behavior, and how execution errors are captured.

Guidelines

  • Keep next(...) deterministic and side-effect light.
  • Return shouldRetry: false when the current attempt should be terminal.
  • Use reason values for debugging and logs.
  • Prefer extending BaseRetryPolicy unless you need full orchestration control.
Edit on GitHub

Last updated on

On this page