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

@ehealth-co-id/typescript-retry-decorator

v3.0.1

Published

A simple retry decorator for typescript with no dependency.

Readme

Retry

A simple retry decorator for typescript with 0 dependency.

This is inspired by the Spring-Retry project. Written in Typescript, 100% Test Coverage.

Import and use it. Retry for Promise is supported as long as the runtime has promise(nodejs/evergreen-browser).

Features:

  • 🎯 Use as decorator (@Retryable) or function wrapper (withRetry)
  • ⏱️ Fixed and exponential backoff strategies
  • 🎲 Jitter support (full, equal, decorrelated) to prevent thundering herd
  • 🚫 Cancellable with AbortSignal support
  • 🎨 Conditional retry with custom logic
  • 📦 Zero dependencies
  • 💯 100% test coverage

Install

npm install typescript-retry-decorator

Options

| Option Name | Type | Required? | Default | Description | |:-----------------:|:------:|:---------:|:---------------------------------------:|:--------------------------------------------------------------------------------------------------------------------------------:| | maxAttempts | number | Yes | - | The max attempts to try | | backOff | number | No | 0 | number in ms to back off. If not set, then no wait | | backOffPolicy | enum | No | FixedBackOffPolicy | can be fixed or exponential | | exponentialOption | object | No | { maxInterval: 2000, multiplier: 2 } | This is for the ExponentialBackOffPolicy The max interval each wait and the multiplier for the backOff. | | doRetry | (e: any) => boolean | No | - | Function with error parameter to decide if repetition is necessary. | | value | Error/Exception class | No | [ ] | An array of Exception types that are retryable. | | reraise | boolean | No | false | If true, rethrows the original error instead of MaxAttemptsError when max attempts is reached. | | signal | AbortSignal | No | - | An AbortSignal to cancel the retry operation. Throws AbortError when aborted. | | useJitter | boolean | No | false | If true, adds random jitter to backoff duration to prevent thundering herd problem. | | jitterType | 'full' | 'equal' | 'decorrelated' | No | 'full' | Type of jitter: full (0 to backOff), equal (backOff/2 to backOff), decorrelated (backOff to 3×backOff). |

Usage

1. As a Decorator

Use @Retryable decorator on class methods:

import { Retryable, BackOffPolicy } from 'typescript-retry-decorator';

class ApiService {
  @Retryable({ maxAttempts: 3 })
  async fetchData(url: string) {
    // This method will be retried up to 3 times on failure
    const response = await fetch(url);
    if (!response.ok) throw new Error('Failed to fetch');
    return response.json();
  }

  @Retryable({ 
    maxAttempts: 3, 
    backOff: 1000,
    backOffPolicy: BackOffPolicy.ExponentialBackOffPolicy
  })
  async uploadFile(file: File) {
    // Retries with exponential backoff: 1s, 2s, 4s
    const formData = new FormData();
    formData.append('file', file);
    const response = await fetch('/upload', { method: 'POST', body: formData });
    if (!response.ok) throw new Error('Upload failed');
    return response.json();
  }
}

2. As a Function Wrapper

Use withRetry to wrap any function:

import { withRetry, BackOffPolicy } from 'typescript-retry-decorator';

// Wrap an existing function
async function fetchUser(userId: string) {
  const response = await fetch(`/api/users/${userId}`);
  if (!response.ok) throw new Error('Failed to fetch user');
  return response.json();
}

const fetchUserWithRetry = withRetry(
  { maxAttempts: 3, backOff: 1000 },
  fetchUser
);

// Use it
const user = await fetchUserWithRetry('123');

// Or wrap inline
const processWithRetry = withRetry(
  { maxAttempts: 5, backOff: 2000 },
  async (data: string) => {
    // Your async operation here
    return await someAsyncOperation(data);
  }
);

Examples

Basic Retry

import { Retryable, withRetry } from 'typescript-retry-decorator';

// Decorator style
class Service {
  @Retryable({ maxAttempts: 3 })
  async fetchData() {
    throw new Error('I failed!');
  }
}

// Function wrapper style
const fetchData = withRetry(
  { maxAttempts: 3 },
  async () => {
    throw new Error('I failed!');
  }
);

Retry with Backoff

// Fixed backoff - wait 1 second between retries
@Retryable({
  maxAttempts: 3,
  backOffPolicy: BackOffPolicy.FixedBackOffPolicy,
  backOff: 1000
})
async fixedBackOffRetry() {
  throw new Error('I failed!');
}

// Exponential backoff - wait 1s, 3s, 9s
@Retryable({
  maxAttempts: 3,
  backOffPolicy: BackOffPolicy.ExponentialBackOffPolicy,
  backOff: 1000,
  exponentialOption: { maxInterval: 10000, multiplier: 3 }
})
async exponentialBackOffRetry() {
  throw new Error('I failed!');
}

Retry Specific Errors

// Only retry on specific error types
@Retryable({ 
  maxAttempts: 3, 
  value: [SyntaxError, ReferenceError]
})
async retrySpecificErrors() {
  throw new SyntaxError('This will retry');
  // throw new TypeError('This will NOT retry');
}

Conditional Retry

// Retry only when custom condition is met
@Retryable({ 
  maxAttempts: 3,
  backOff: 1000,
  doRetry: (e: Error) => {
    // Only retry on 429 (Too Many Requests) or 503 (Service Unavailable)
    return e.message.includes('429') || e.message.includes('503');
  }
})
async conditionalRetry() {
  throw new Error('Error: 429 Too Many Requests');
}

Reraise Original Error

// By default, MaxAttemptsError is thrown with the original error wrapped
// Use reraise: true to throw the original error instead
@Retryable({ 
  maxAttempts: 3,
  reraise: true  // Throw original error, not MaxAttemptsError
})
async reraiseExample() {
  throw new Error('Original error');
}

try {
  await service.reraiseExample();
} catch (error) {
  // error is the original Error, not MaxAttemptsError
  console.log(error.message); // "Original error"
}

Jitter to Prevent Thundering Herd

import { withRetry, BackOffPolicy } from 'typescript-retry-decorator';

// Full Jitter - Random backoff between 0 and backOff duration
// Provides maximum randomization to spread out retry attempts
const fetchWithFullJitter = withRetry(
  { 
    maxAttempts: 5, 
    backOff: 2000,
    useJitter: true,
    jitterType: 'full'  // Backoff will be 0-2000ms randomly
  },
  async (url: string) => {
    const response = await fetch(url);
    if (!response.ok) throw new Error('Failed');
    return response.json();
  }
);

// Equal Jitter - Random backoff between backOff/2 and backOff
// Maintains minimum wait time while adding randomness
const fetchWithEqualJitter = withRetry(
  { 
    maxAttempts: 5, 
    backOff: 2000,
    useJitter: true,
    jitterType: 'equal'  // Backoff will be 1000-2000ms randomly
  },
  fetchData
);

// Decorrelated Jitter - Can increase backoff beyond base duration
// More aggressive randomization for heavily loaded systems
const fetchWithDecorrelatedJitter = withRetry(
  { 
    maxAttempts: 5, 
    backOff: 1000,
    useJitter: true,
    jitterType: 'decorrelated'  // Backoff will be 1000-3000ms randomly
  },
  fetchData
);

// Jitter with Exponential Backoff
// Combines exponential growth with randomization
@Retryable({
  maxAttempts: 5,
  backOff: 1000,
  backOffPolicy: BackOffPolicy.ExponentialBackOffPolicy,
  exponentialOption: { maxInterval: 30000, multiplier: 2 },
  useJitter: true,
  jitterType: 'full'
})
async apiCallWithJitter() {
  // First retry: 0-1000ms
  // Second retry: 0-2000ms
  // Third retry: 0-4000ms
  // etc.
}

Cancellable Retry with AbortSignal

import { withRetry, AbortError } from 'typescript-retry-decorator';

// Create an abort controller
const controller = new AbortController();

// Function wrapper with signal
const fetchWithRetry = withRetry(
  { 
    maxAttempts: 10, 
    backOff: 2000,
    signal: controller.signal  // Pass the abort signal
  },
  async (url: string) => {
    const response = await fetch(url);
    if (!response.ok) throw new Error('Failed');
    return response.json();
  }
);

// Cancel after 5 seconds
setTimeout(() => controller.abort(), 5000);

try {
  const data = await fetchWithRetry('https://api.example.com/data');
} catch (error) {
  if (error instanceof AbortError) {
    console.log('Retry operation was cancelled');
  }
}

// Also works with decorator
const controller2 = new AbortController();

class Service {
  @Retryable({ 
    maxAttempts: 5, 
    backOff: 1000,
    signal: controller2.signal 
  })
  async fetchData() {
    // Will be cancelled when controller2.abort() is called
  }
}

Real-world Example

import { withRetry, BackOffPolicy, MaxAttemptsError, AbortError } from 'typescript-retry-decorator';

class ApiClient {
  private baseUrl = 'https://api.example.com';

  // Decorator on class method
  @Retryable({
    maxAttempts: 3,
    backOff: 1000,
    backOffPolicy: BackOffPolicy.ExponentialBackOffPolicy,
    exponentialOption: { maxInterval: 5000, multiplier: 2 },
    doRetry: (e: Error) => {
      // Retry on network errors or 5xx server errors
      return e.message.includes('network') || e.message.includes('5');
    }
  })
  async get(endpoint: string) {
    const response = await fetch(`${this.baseUrl}${endpoint}`);
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    return response.json();
  }

  // Function wrapper with cancellation
  async getWithCancellation(endpoint: string, timeoutMs: number) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), timeoutMs);

    const fetchWithRetry = withRetry(
      {
        maxAttempts: 5,
        backOff: 1000,
        backOffPolicy: BackOffPolicy.ExponentialBackOffPolicy,
        signal: controller.signal,
        reraise: false
      },
      async () => {
        const response = await fetch(`${this.baseUrl}${endpoint}`);
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        return response.json();
      }
    );

    try {
      const data = await fetchWithRetry();
      clearTimeout(timeoutId);
      return data;
    } catch (error) {
      clearTimeout(timeoutId);
      if (error instanceof AbortError) {
        console.log('Request cancelled due to timeout');
      } else if (error instanceof MaxAttemptsError) {
        console.log(`Failed after ${error.retryCount} attempts`);
      }
      throw error;
    }
  }
}

API Reference

Exports

// Main functions
export function Retryable(options: RetryOptions): DecoratorFunction;
export function withRetry<T extends (...args: any[]) => any>(
  options: RetryOptions,
  fn: T
): T;

// Error classes
export class MaxAttemptsError extends Error {
  code: string;
  retryCount: number;
  originalError: Error;
}

export class AbortError extends Error {
  code: string;
  name: string;
}

// Enums
export enum BackOffPolicy {
  FixedBackOffPolicy = 'FixedBackOffPolicy',
  ExponentialBackOffPolicy = 'ExponentialBackOffPolicy'
}

// Interfaces
export interface RetryOptions {
  maxAttempts: number;
  backOffPolicy?: BackOffPolicy;
  backOff?: number;
  doRetry?: (e: any) => boolean;
  value?: ErrorConstructor[];
  exponentialOption?: { maxInterval: number; multiplier: number };
  reraise?: boolean;
  signal?: AbortSignal;
  useJitter?: boolean;
  jitterType?: 'full' | 'equal' | 'decorrelated';
}

export type JitterType = 'full' | 'equal' | 'decorrelated';

Common Use Cases

API Rate Limiting

const apiCall = withRetry(
  {
    maxAttempts: 5,
    backOff: 1000,
    backOffPolicy: BackOffPolicy.ExponentialBackOffPolicy,
    doRetry: (e: Error) => e.message.includes('429')
  },
  async () => await fetch('/api/data')
);

Network Resilience

@Retryable({
  maxAttempts: 3,
  backOff: 2000,
  value: [TypeError, NetworkError], // Retry only on network errors
  exponentialOption: { maxInterval: 10000, multiplier: 2 }
})
async fetchFromUnstableService() {
  // Your code here
}

Preventing Thundering Herd in Microservices

// When multiple service instances fail simultaneously,
// jitter prevents them all from retrying at the exact same time
@Retryable({
  maxAttempts: 5,
  backOff: 2000,
  backOffPolicy: BackOffPolicy.ExponentialBackOffPolicy,
  exponentialOption: { maxInterval: 30000, multiplier: 2 },
  useJitter: true,
  jitterType: 'full'  // Spreads retry attempts across time
})
async callDownstreamService(serviceUrl: string) {
  const response = await fetch(serviceUrl);
  if (!response.ok) throw new Error(`Service error: ${response.status}`);
  return response.json();
}

User-Cancellable Operations

const controller = new AbortController();

// Show cancel button to user
document.getElementById('cancelBtn').onclick = () => controller.abort();

const operation = withRetry(
  { maxAttempts: 10, backOff: 1000, signal: controller.signal },
  async () => await longRunningOperation()
);

License

MIT