# ts-retry

A little retry tool to execute a function until the function is successful. Can also bind a timeout to a function.
This lib is usable in typescript, in javascript, in node, in SPA tools (rest, Vue, Svelte...) and browser (available in ESM and common js format).

---

**Recent Changes (since v5.0.0+)**:

- **v6.0.0**: `onError` callback can now return a boolean to control retry behavior:
  - Returning `true` or nothing: continue retries (default behavior)
  - Returning `false`: stop retries and throw an `AbortError`
- **AbortError**: New error type thrown when retries are aborted via `onError` returning `false`. Includes methods `getError()` (last error) and `getCurrentTry()` (attempt number).
- **v5.0.0**: `onError` and `onSuccess` callbacks now receive attempt data (`currentTry`) for better logging.
- **v5.0.1**: Fixed missing exports for `retryDecorator` and `retryAsyncDecorator`.

**Breaking change**:
To migrate to 3.x: retryAsyncDecorator and retryAsync has been moved to utils/decorators. This impacts only
those that import those functions directly from decorator.ts file.
Other 3.X items are new and imply no breaking change.

For those who are using 1.x in **typescript**, you may have to add a type to RetryOptions if you want to use
the new `until` function. This type is the called function's return type.

## How to

- to retry something:

  ```javascript
  const result = await retry(
    () => {
      /* do something */
    },
    { delay: 100, maxTry: 5 },
  );
  ```

- to retry something async :

  ```javascript
  const result = await retryAsync(
    async () => {
      /* do something */
    },
    { delay: 100, maxTry: 5 },
  );
  ```

- to retry until the answer is 42 :

  ```javascript
  try {
    await retryAsync(
      async () => {
        /* do something */
      },
      {
        delay: 100,
        maxTry: 5,
        until: (lastResult) => lastResult === 42,
      },
    );
  } catch (err) {
    if (isTooManyTries(err)) {
      // Did not get 42 after 'maxTry' calls
    } else {
      // something else goes wrong
    }
  }
  ```

- to retry with custom error handling and potential abort:

  ```javascript
  try {
    await retryAsync(
      async () => {
        /* do something that might fail */
      },
      {
        delay: 100,
        maxTry: 5,
        onError: (err, currentTry) => {
          console.log(`Attempt ${currentTry} failed: ${err.message}`);
          // Abort if it's a specific error type
          return err.message.includes("fatal") ? false : true;
        },
      },
    );
  } catch (err) {
    if (isAbortError(err)) {
      // Retries were aborted
      console.log(`Aborted at attempt ${err.getCurrentTry()}`);
    } else if (isTooManyTries(err)) {
      // Max tries reached
    } else {
      // Other error
    }
  }
  ```

- Need to call a function at multiple locations with the same retryOptions? Use decorators:

  ```javascript
  const fn = (param1: string, param2: number) => /* do something */;
  const decoratedFn = retryDecorator(
    fn,
    { delay:100, maxTry:5 }
  );
  const result1 = await decoratedFn("value1", 1);
  const result2 = await decoratedFn("valueXXX", 2);

  const fn = async (name: string): Promise<any> => { /* something async */ };
  const decoratedFn = retryAsyncDecorator(
    fn,
    { delay:100, maxTry:5 }
  );
  const result1 = await decoratedFn("Smith");
  const result2 = await decoratedFn("Doe");
  ```

- to wait:

  ```javascript
  await wait(10000); // Wait for 10 seconds
  ```

- to set a timeout:

  ```javascript
  try {
    const result = await waitUntil(() => {
      /* do something */
    }, 10000);
  } catch (err) {
    if (isTimeoutError(err)) {
      // fn does not complete after 10 seconds
    } else {
      // the function throws an exception
    }
  }
  ```

- to set a timeout on something async:

  ```javascript
  try {
    const result = await waitUntilAsync(async () => {
      /* do something */
    }, 10000);
  } catch (err) {
    if (isTimeoutError(err)) {
      // fn does not complete after 10 seconds
    } else {
      // fn throws an exception
    }
  }
  ```

- Need to call a function at multiple locations with the same retryOptions? Use decorators:

  ```javascript
    const fn = (title: string, count: number) => /* a long task */;
    const decoratedFn = waitUntilDecorator(
      fn,
      { delay:100, maxTry:5 }
    );
    const result1 = await decoratedFn("Intro", 1);
    const result2 = await decoratedFn("A chapter", 2);
  ```

  ```javascript
  const fn = async (name: string): Promise<any> => {
    /* a long task */
  };
  const decoratedFn = waitUntilAsyncDecorator(fn, { delay: 100, maxTry: 5 });
  const result1 = await decoratedFn("John");
  const result2 = await decoratedFn("Doe");
  ```

---

## Utils

`retry` comes with handy utility functions for common use cases:

- to retry until a function returns something defined (i.e., not null nor undefined):

```typescript
  // in all cases the result is a string and cannot be null or undefined
  const result = await retryUntilDefined( (): string|undefined => { ... } );

  const result = await retryUntilAsyncDefined( (): Promise<string|null> => { ... } );

  const decorated = retryUntilDefinedDecorator( (p1: string): string|undefined => { ... } );
  const result = await decorated('hello world');

  const decorated = retryAsyncUntilDefinedDecorator( (p1: string): Promise<string|undefined> => { ... } );
  const result = await decorated('hello world');
```

- to retry until a function returns something truthy:

```typescript
  // in all cases the result is a string and cannot be null or undefined
  const result = await retryUntilTruthy( (): boolean|undefined => { ... } );

  const result = await retryAsyncUntilTruthy( (): Promise<number|null> => { ... } );

  const decorated = retryUntilTruthyDecorator( (p1: string): boolean|undefined => { ... } );
  const result = await decorated('hello world');

  const decorated = retryAsyncUntilTruthyDecorator( (p1: string): Promise<boolean|null> => { ... } );
  const result = await decorated('hello world');
```

- to retry until fetch is successful:

```typescript
  const result = await retryAsyncUntilResponse( () => fetch(...) );

  const decorated = retryAsyncUntilResponseDecorator( (param) => fetch(...) );
  const result = await decorated('q=1');
```

---

## API

### Retry family

- `retry(fn, retryOptions?)`: call repeatedly `fn` until `fn` does not throw an exception. Stop after the `retryOptions.maxTry` count. Between each call wait `retryOptions.delay` milliseconds.
  If we stop calling fn after `retryOptions.maxTry`, throw the `fn` exception, otherwise return the fn return value.
- `retryAsync(fn, retryOptions?)`: same as retry, except `fn` is an asynchronous function.
- `retryOptions`:
  - `maxTry`: [optional] maximum calls to fn.
  - `delay`: [optional] delay between each call (in milliseconds). Could be either a number or a function (when delay time depends on the number of retries, or previous result...), see below for explanation about delay
  - `until`: [optional] (lastResult) => boolean: return false if the last `fn` result is not the expected one: continue calling fn until `until` returns true. A `TooManyTries` is thrown after `maxTry` calls to fn;
  - `onError`: [optional](err: Error, currentTry: number) => boolean | void: called on each error except the last one. Includes the current try for logging. Return `true` or nothing to continue retries, `false` to abort and throw `AbortError`. To catch/log the last error, use onMaxRetryFunc
- `onMaxRetryFunc`: [optional](err: Error) => void: called on the final error at the maxTry limit only
- `onSuccess`: [optional](currentTry: number) => void: called on success. Includes the current try for logging
  When an option value is not provided, the default one is applied. The default options are:

```javascript
  delay: 250,
  maxTry: 4 * 60,
```

- `setDefaultRetryOptions<T>(retryOptions: RetryOptions<T>)`: change the default retryOptions.
- `getDefaultRetryOptions<T>()`: returns the current default retry options.
- `retryAsyncDecorator<T>(fn: T, retryOptions?: RetryOptions<T>)` and `retryDecorator<T>(fn: T, retryOptions?: RetryOptions<T>)`: decorators that return a function with the same signature as the given function. On decorated call, fn is called repeatedly until it does not throw an exception or until retryOptions.maxTry.
- `TooManyTries`: an error thrown by retry functions when `until` returns false after `maxTry` calls. It comes with a type guard and includes the last failed result:

```javascript
if (isTooManyTries(error)) {
  // retry failed
  console.error(`last error is ${error.getLastResult()}`);
}
```

- `AbortError`: an error thrown when retries are aborted via `onError` returning `false`. It comes with methods to access the last error and attempt number:

```javascript
if (isAbortError(error)) {
  // retry aborted
  console.error(
    `Aborted at attempt ${error.getCurrentTry()}, last error: ${error.getError()}`,
  );
}
```

### When delay can vary

When delay option is a function, it is called before each retry: this allows having a delay that can change between retries (e.g., delay can increase exponentially).
The function receives the following parameters:

```javascript
(parameter: {
  currentTry: number,
  maxTry: number,
  lastDelay?: number
  lastResult?: RETURN_TYPE,
  lastError?: Error
}) => number;
```

where:

- `currentTry`: the number of calls to fn (first is 1, not 0).
- `maxTry`: maximum calls to fn.
- `lastDelay`: the previous delay, undefined when no delay has been computed yet.
- `lastResult`: the last result, undefined if the last call to fn failed
- `lastError`: the last error, undefined if the last call to fn succeeded

## Until family

`retry` comes with handy utility functions for common use cases:

**UntilDefined :**
To retry until we get a value which is neither null nor undefined.

For calling sync function:

```typescript
retryUntilDefined<RETURN_TYPE>(
  fn: () => RETURN_TYPE | undefined | null,
  retryOptions?: RetryUtilsOptions,
): Promise<RETURN_TYPE>
```

```typescript
retryUntilDefinedDecorator<PARAMETERS_TYPE, RETURN_TYPE>(
  fn: (...args: PARAMETERS_TYPE) => RETURN_TYPE | undefined | null,
  retryOptions?: RetryUtilsOptions,
): (...args: PARAMETERS_TYPE) => Promise<RETURN_TYPE>
```

For calling async function:

```typescript
retryAsyncUntilDefined<RETURN_TYPE>(
  fn: () => Promise<RETURN_TYPE | undefined | null>,
  options?: RetryUtilsOptions,
): Promise<RETURN_TYPE>
```

```typescript
retryAsyncUntilDefinedDecorator<PARAMETERS_TYPE, RETURN_TYPE>(
  fn: (...args: PARAMETERS_TYPE) => Promise<RETURN_TYPE | undefined | null>,
  retryOptions?: RetryUtilsOptions,
): (...args: PARAMETERS_TYPE) => Promise<RETURN_TYPE>
```

**UntilTruthy :**
To retry until we get a value which JavaScript considers truthy.

For calling sync function:

```typescript
retryUntilTruthy<PARAMETERS_TYPE, RETURN_TYPE>(
  fn: (...args: PARAMETERS_TYPE) => RETURN_TYPE,
  retryOptions?: RetryUtilsOptions,
): Promise<RETURN_TYPE>
```

```typescript
retryUntilTruthyDecorator<PARAMETERS_TYPE, RETURN_TYPE>(
  fn: (...args: PARAMETERS_TYPE) => RETURN_TYPE,
  retryOptions?: RetryUtilsOptions,
): (...args: PARAMETERS_TYPE) => Promise<RETURN_TYPE>
```

For calling async function:

```typescript
retryAsyncUntilTruthy<PARAMETERS_TYPE, RETURN_TYPE>(
  fn: (...args: PARAMETERS_TYPE) => Promise<RETURN_TYPE>,
  retryOptions?: RetryUtilsOptions,
): Promise<RETURN_TYPE>
```

```typescript
retryAsyncUntilTruthyDecorator<PARAMETERS_TYPE, RETURN_TYPE>(
  fn: (...args: PARAMETERS_TYPE) => Promise<RETURN_TYPE>,
  retryOptions?: RetryUtilsOptions,
): (...args: PARAMETERS_TYPE) => Promise<RETURN_TYPE>
```

**UntilResponse :**
To retry until fetch is successful.

```typescript
retryAsyncUntilResponse<PARAMETERS_TYPE, RETURN_TYPE extends { ok: boolean }>(
  fn: () => Promise<RETURN_TYPE>,
  retryOptions?: RetryUtilsOptions,
): Promise<RETURN_TYPE>
```

```typescript
retryAsyncUntilResponseDecorator<PARAMETERS_TYPE, RETURN_TYPE extends { ok: boolean }>(
  fn: (...args: PARAMETERS_TYPE) => Promise<RETURN_TYPE>,
  retryOptions?: RetryUtilsOptions,
): (...args: PARAMETERS_TYPE) => Promise<RETURN_TYPE>
```

`RetryUtilsOptions` type is the same as `RetryOptions` but without the `until` option.

## Delay family

**createExponentialDelay**
Returns a delay function that provides exponential delays.

```javascript
const delay = createExponentialDelay(20);
const result = await retryAsync(
  async () => {
    /* do something */
  },
  { delay, maxTry: 5 },
);
```

delay between each try will return 20, 400, 8000, 160000, 3200000

**createMultiplicativeDelay**
Returns a delay function that provides multiplicative delays:

```typescript
createMultiplicativeDelay<RETURN_TYPE>(initialDelay: number, multiplicator: number)
createMultiplicableDelay<RETURN_TYPE>(initialDelay: number, multiplier: number)
```

First delay returns initialDelay, second initialDelay*multiplicator, third multiplicator * initialDelay _ (multiplicator _ 2) and so on

```javascript
const delay = createMultiplicableDelay(20);
const result = await retryAsync(
  async () => {
    /* do something */
  },
  { delay, maxTry: 5 },
);
```

delay will be 20, 60, 120, 180, 240

**createRandomDelay**
Returns a delay function that provides random delays between given min and max (included):

```typescript
createRandomDelay<RETURN_TYPE>(min: number, max: number)
```

Each time the created delay is called, a value between min and max (both included) is generated

```javascript
const delay = createRandomDelay(500, 10000);
const result = await retryAsync(
  async () => {
    /* do something */
  },
  { delay, maxTry: 5 },
);
```

delay between each try will be a random value between 500 and 10000 ms.

## Wait family

- `wait(duration?)`: Does nothing for "duration" milliseconds
- `waitUntil(fn, duration?, error?)`: waitUntil calls fn asynchronously once. If fn completes within the duration (expressed in milliseconds), waitUntil returns the fn result. Otherwise, it throws the given error (if any) or a TimeoutError exception.
- `waitUntilAsync(fn, duration?, error?)`: same as waitUntil, except fn is an asynchronous function.
- `TimeoutError`: an error thrown by waitUntil and waitUntilAsync. It comes with an isTimeoutError type guard:

```javascript
if (isTimeoutError(error)) {
  // fn does not complete within 10 seconds
}
```

In case of timeout fn is still executing. It is advised to add a means to abort it.

- When duration is not provided, the default one is applied. The default is 60000ms.
- `setDefaultDuration(duration: number)`: change the default duration.
- `getDefaultDuration()`: returns the current default duration.
- `waitUntilAsyncDecorator(fn: T, duration?: number, error?: Error)` and `waitUntilDecorator(fn: T, duration?: number, error?: Error)`: decorators that return a function with the same signature as the given function. On decorated call, fn is called bound to the duration.

## Custom reaction when max retry is achieved

Sometimes, you need to perform some actions when max retry is achieved and the error is still there. For this `onMaxRetryFunc?: (err: Error, currentTry: number) => void;` optional function was added to `RetryOptions`.
For example, you would like to store results of the error into the file in order to process it later. Here's how you can do it :

```typescript
export const runWithRetry = <T>(
  message: string,
  serviceUnderTest: ServiceUnderTest,
  fn: () => T | Promise<T>,
  delay = 1000,
  maxTry = 10,
) => {
  const saveErrorReport = (err, currentTry) => {
    const errorDetails = {
      serviceName: serviceUnderTest.connectorName,
      error: err.message as string,
      description: `Failed to ${message} because of ${err.message as string}`,
      errorName: err.name as string,
      stack: err.stack as string,
      currentTry,
    };
    const path = resolve(
      __dirname,
      `../../../failed-service-report/${serviceUnderTest.connectorName}.json`,
    );
    writeFile(path, Buffer.from(JSON.stringify(errorDetails)));
  };
  return retryAsync(
    async () => {
      logger.info(`${serviceUnderTest.description}: ${message}`);
      return fn();
    },
    {
      delay,
      maxTry,
      onMaxRetryFunc: saveErrorReport,
    },
  );
};
```

## Compatibility

This lib works with Deno (to import it, use the URL `https://raw.githubusercontent.com/franckLdx/ts-retry/<version>/src/index.ts`). However, it's more convenient to use the specific port of this lib to Deno: see `https://deno.land/x/retry`
