Skip to content

Latest commit

Β 

History

14 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Smart-Retry

Smart utility to retry flaky sync/async operations with delay, backoff, jitter, abort, and retry hooks.

License: MIT

✨ Features

  • βœ… Works with sync and async functions
  • πŸ” Configurable number of retries and delay
  • ⏱️ Exponential backoff and custom schedules
  • 🎲 Jitter and randomized backoff strategies
  • πŸ›‘ Multiple cancellation options (AbortSignal, predicate, token)
  • πŸ” onRetry hook for logging or metrics
  • ⚠️ Optional error filtering with shouldRetry
  • 🧩 Retry until condition or with custom delay logic
  • πŸͺΆ Zero dependencies

πŸ“¦ Install

npm install smart-retry

⚑ Quick Usage

import { retry } from "smart-retry";

async function fetchData() {
  const res = await fetch("https://example.com/api");
  if (!res.ok) throw new Error("API error"); // by default, all errors are retryable
  return res.json();
}

const result = await retry(() => fetchData(), {
  retries: 3,
  delay: 1000,
  factor: 2,
  onRetry: (err, attempt) => {
    console.warn(`Attempt #${attempt} failed: ${err}`);
  },
});

πŸ”„ How Retry Works

The retry mechanism in this library follows a simple but powerful pattern:

  1. Execution: The library attempts to run your function (sync or async).
  2. Error Handling:
    • If the function succeeds (returns a value without throwing), the result is returned immediately.
    • If the function throws an error:
      • The library checks if another retry should be attempted based on the retries count and shouldRetry predicate.
      • If no more retries are allowed or shouldRetry returns false, the error is thrown.
  3. Notification: If a retry is needed, the onRetry callback is invoked with the error and attempt number.
  4. Delay: The library waits for a specified delay duration before the next attempt.
    • For regular retry: The delay increases exponentially by the specified factor.
    • For jitter: Random variation is added to prevent "thundering herd" problems.
    • For scheduled retries: The delay follows a pre-defined array of values.
  5. Cancellation: Some retry functions support cancellation via:
    • AbortSignal: Standard web API for cancellation
    • Predicate function: Custom function that returns true when retries should stop
    • Cancellation token: Object with a boolean property to cancel retries

All retry functions can handle both synchronous and asynchronous target functions, automatically wrapping results in promises for a consistent async interface.


βš™οΈ API Overview

Common Options

Option Type Default Description
retries number 3 Number of retry attempts
delay number (ms) 500 Initial delay between retries
factor number 2 Multiplier for exponential backoff
onRetry (err, attempt) => void - Hook called on every failed retry
shouldRetry (err) => boolean always Predicate to decide if error is retryable

Provided Functions

  • retry: Standard exponential backoff retry.
  • retryWithJitter: Adds random jitter to delay.
  • retryWithTimeout: Retries until a total timeout is reached.
  • retryWithSchedule: Retries using a custom array of delays.
  • retryWithAbortSignal: Supports aborting with an AbortSignal.
  • retryWithPredicate: Stops retrying when a predicate function returns true.
  • retryWithCancellationToken: Stops retrying when a token object's cancelled property is true.
  • retryWithMaxTotalAttempts: Specify max total attempts (not just retries).
  • retryWithPredicateDelay: Custom delay logic per attempt.
  • retryWithRandomizedBackoff: Decorrelated jitter/randomized backoff.
  • retryUntilCondition: Retries until a user-provided predicate returns true.
  • retryWithCustomExecution: Uses custom execution logic instead of try/catch to evaluate success.

πŸ§‘β€πŸ’» Examples

Retry a Synchronous Function

import { retry } from "smart-retry";

let count = 0;
const result = await retry(() => {
  count++;
  if (count < 3) throw new Error("Oops");
  return "done";
});

console.log(result); // Output: 'done' after 3 attempts

Retry Until Condition

import { retryUntilCondition } from "smart-retry";

// Example 1: Wait for a value to reach a threshold
let value = 0;
await retryUntilCondition(
  () => ++value,
  (result) => result === 5,
  { retries: 10, delay: 100 },
);

// Example 2: Poll an API until a resource is ready
async function checkStatus() {
  const res = await fetch("https://api.example.com/status");
  const data = await res.json();
  return data.status;
}

await retryUntilCondition(
  () => checkStatus(),
  (status) => status === "ready",
  { retries: 20, delay: 500, onRetry: (err, attempt) => console.log(`Attempt ${attempt}: not ready yet`) },
);

Retry an Async Function with Exponential Backoff and Error Filtering

import { retry } from "smart-retry";

await retry(() => fetchSomething(), {
  retries: 5,
  delay: 500,
  factor: 1.5,
  shouldRetry: (err) => err instanceof TimeoutError,
});

Retry with Jitter

import { retryWithJitter } from "smart-retry";

await retryWithJitter(() => fetchSomething(), {
  retries: 4,
  delay: 200,
  jitter: 100,
  onRetry: (err, attempt) => {
    console.log(`Retry #${attempt} failed: ${err}`);
  },
});

Retry with AbortSignal

import { retryWithAbortSignal } from "smart-retry";

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

try {
  await retryWithAbortSignal(() => fetchSomething(), {
    retries: 10,
    delay: 200,
    signal: controller.signal,
  });
} catch (err) {
  if (controller.signal.aborted) {
    console.log("Retry aborted by user.");
  } else {
    console.error("Retry failed:", err);
  }
}

Retry with Predicate

import { retryWithPredicate } from "smart-retry";

// Flag to stop retrying when set to true
let stopRetrying = false;

// Set up a separate timeout that will stop retries after 1 second
setTimeout(() => {
  stopRetrying = true;
}, 1000);

try {
  await retryWithPredicate(
    () => fetchFromSlowAPI(),
    () => stopRetrying, // Stop when our flag becomes true
    { retries: 10, delay: 200 },
  );
} catch (err) {
  if (stopRetrying) {
    console.log("Retry stopped by predicate");
  } else {
    console.error("Retry failed:", err);
  }
}

Retry with Cancellation Token

import { retryWithCancellationToken } from "smart-retry";

// Create a token object with a 'cancelled' property
const token = { cancelled: false };

// Set up a separate process that can cancel the operation
setTimeout(() => {
  token.cancelled = true;
}, 1000);

try {
  await retryWithCancellationToken(() => fetchSomething(), {
    retries: 5,
    delay: 200,
    cancellationToken: token,
  });
} catch (err) {
  if (token.cancelled) {
    console.log("Retry cancelled via token");
  } else {
    console.error("Retry failed:", err);
  }
}

Retry with Custom Schedule

import { retryWithSchedule } from "smart-retry";

// Will retry after 100ms, then 200ms, then 400ms, then 800ms
await retryWithSchedule(() => fetchSomething(), [100, 200, 400, 800]);

Retry with Predicate Delay

import { retryWithPredicateDelay } from "smart-retry";

await retryWithPredicateDelay(
  () => fetchSomething(),
  (err, attempt) => attempt * 100, // delay increases per attempt
  { retries: 5 },
);

Retry with Randomized Backoff

import { retryWithRandomizedBackoff } from "smart-retry";

await retryWithRandomizedBackoff(() => fetchSomething(), {
  retries: 5,
  delay: 100,
  maxDelay: 2000,
  onRetry: (err, attempt) => {
    console.log(`Randomized backoff retry #${attempt}: ${err}`);
  },
});

Retry with Max Total Attempts

import { retryWithMaxTotalAttempts } from "smart-retry";

let tries = 0;
await retryWithMaxTotalAttempts(
  () => {
    tries++;
    if (tries < 4) throw new Error("Still failing");
    return "success";
  },
  5, // max total attempts (including the first)
  { delay: 100 },
);

Retry with Custom Execution

You can also use a custom execution function to handle the retry logic. This is useful when you want to control how the success or failure of the operation is determined.

import { retryWithCustomExecution, ExecutionResult } from "smart-retry";

// Create a custom execution function that checks return values
// instead of relying on exceptions
async function customAPIExecute<T>(fn: () => Promise<Response>): Promise<ExecutionResult<T>> {
  try {
    const response = await fn();
    // Consider HTTP 2xx status codes as success
    if (response.status >= 200 && response.status < 300) {
      return {
        success: true,
        value: response as unknown as T,
      };
    }
    // Consider any other status code as a failure
    return {
      success: false,
      error: new Error(`API returned status: ${response.status}`),
      value: response as unknown as T, // You can still include the value for the retry handler
    };
  } catch (error) {
    // Handle network errors or other exceptions
    return { success: false, error: error as Error };
  }
}

// Use the custom execution logic with retries
try {
  const result = await retryWithCustomExecution<Response>(() => fetch("https://api.example.com/data"), {
    execute: customAPIExecute,
    retries: 3,
    delay: 200,
    onRetry: (err, attempt) => {
      console.log(`Custom execution retry #${attempt}: ${err.message}`);
    },
  });

  // Process successful result
  const data = await result.json();
} catch (err) {
  console.error("All retry attempts failed:", err);
}

πŸ“„ License

MIT Β© jaktestowac.pl

Powered by jaktestowac.pl team!

🌐 Check out GitHub profile for more open-source projects and resources.

About

πŸ“¦ Smart utility to retry flaky sync/async operations with delay, backoff, and retry hooks.

Topics

Resources

Stars

1 star

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages