{"id":21952173,"url":"https://github.com/stefanmaric/retryyy","last_synced_at":"2025-04-23T04:38:30.277Z","repository":{"id":255436358,"uuid":"842044370","full_name":"stefanmaric/retryyy","owner":"stefanmaric","description":"A better way to retry async operations in TypeScript/JavaScript.","archived":false,"fork":false,"pushed_at":"2024-09-03T14:52:04.000Z","size":112,"stargazers_count":5,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-10T13:53:49.290Z","etag":null,"topics":["async","await","decorators","functional-programming","retry","typesafe","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/stefanmaric.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":".github/CONTRIBUTING.md","funding":null,"license":"LICENSE.md","code_of_conduct":".github/CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":".github/SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-08-13T14:58:05.000Z","updated_at":"2025-01-05T16:40:04.000Z","dependencies_parsed_at":null,"dependency_job_id":"39fadd39-fd3b-4367-b8c8-7b6dee12e68a","html_url":"https://github.com/stefanmaric/retryyy","commit_stats":null,"previous_names":["stefanmaric/retryyy"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stefanmaric%2Fretryyy","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stefanmaric%2Fretryyy/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stefanmaric%2Fretryyy/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stefanmaric%2Fretryyy/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/stefanmaric","download_url":"https://codeload.github.com/stefanmaric/retryyy/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250372692,"owners_count":21419720,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["async","await","decorators","functional-programming","retry","typesafe","typescript"],"created_at":"2024-11-29T06:24:03.061Z","updated_at":"2025-04-23T04:38:30.264Z","avatar_url":"https://github.com/stefanmaric.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# retryyy\n\nA better way to retry async operations in TypeScript/JavaScript.\n\n\u003cp\u003e\n\t\u003ca href=\"https://github.com/stefanmaric/retryyy/blob/main/.github/CODE_OF_CONDUCT.md\" target=\"_blank\"\u003e\u003cimg alt=\"🤝 Code of Conduct: Kept\" src=\"https://img.shields.io/badge/%F0%9F%A4%9D_code_of_conduct-kept-21bb42\" /\u003e\u003c/a\u003e\n\t\u003ca href=\"https://codecov.io/gh/stefanmaric/retryyy\" target=\"_blank\"\u003e\u003cimg alt=\"🧪 Coverage\" src=\"https://img.shields.io/codecov/c/github/stefanmaric/retryyy?label=%F0%9F%A7%AA%20coverage\" /\u003e\u003c/a\u003e\n\t\u003ca href=\"https://github.com/stefanmaric/retryyy/blob/main/LICENSE.md\" target=\"_blank\"\u003e\u003cimg alt=\"📝 License: MIT\" src=\"https://img.shields.io/badge/%F0%9F%93%9D_license-MIT-21bb42.svg\"\u003e\u003c/a\u003e\n\t\u003ca href=\"http://npmjs.com/package/retryyy\"\u003e\u003cimg alt=\"📦 npm version\" src=\"https://img.shields.io/npm/v/retryyy?color=21bb42\u0026label=%F0%9F%93%A6%20npm\" /\u003e\u003c/a\u003e\n\t\u003cimg alt=\"💪 TypeScript: Strict\" src=\"https://img.shields.io/badge/%F0%9F%92%AA_typescript-strict-21bb42.svg\" /\u003e\n\u003c/p\u003e\n\n---\n\n## Highlights\n\n- 🪄 **Easy**: Handy defaults and easily configurable.\n- 🪶 **Lightweight**: Only 619 bytes core (417B gzipped). Get all the goodies for 2.6kb (1.3kB gzipped).\n- 📦 **Complete**: Includes circuit breaker, exponential backoff, timeout, jitter, logging, branded errors, and more.\n- 🌟 **Modern**: Leverage modern standards like `AbortSignal`, `AggregateError`, [decorators](https://2ality.com/2022/10/javascript-decorators.html), and [ESM](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c).\n- 🧘 **Simple**: More than a library, `retryyy` is a pattern for retry control-flow.\n- 🔗 **Composable**: Policies are functions that can be chained together like middlewares.\n- 🔐 **Type-safe**: Safely wrap your existing TypeScript functions in retry logic.\n\n## Setup\n\nInstall it from npm with your preferred package manager:\n\n```shell\npnpm add retryyy\n```\n\n```shell\nnpm install retryyy\n```\n\n```shell\nyarn add retryyy\n```\n\n```shell\nbun add retryyy\n```\n\n## Usage\n\n```javascript\nimport { retryyy } from 'retryyy'\n\nretryyy(async () =\u003e {\n  const res = await fetch(`https://jsonplaceholder.typicode.com/users/${1}`)\n  const user = await res.json()\n  console.log(user)\n})\n```\n\nIt will retry the provided async functions using the [Default policy](./src/policies/Default.ts).\n\n### Customizing the default policy\n\nAn object can be passed as a second argument to `retryyy()` to customize the behavior of the default policy.\n\n```javascript\nimport { retryyy } from 'retryyy'\n\nretryyy(\n  async () =\u003e {\n    // do stuff...\n  },\n  {\n    timeout: 10_000, // Shorter timeout; 10 seconds.\n  },\n)\n```\n\n#### Options\n\n| Option         | Description                                         | Default         |\n| -------------- | --------------------------------------------------- | --------------- |\n| `fastTrack`    | If true, runs the first re-attempt immediately.     | `false`         |\n| `initialDelay` | The initial delay in milliseconds.                  | 150ms           |\n| `logError`     | Logger function to use when giving up on retries.   | `console.error` |\n| `logWarn`      | Logger function to use when retrying.               | `console.warn`  |\n| `maxAttempts`  | The maximum number of attempts to make.             | 10              |\n| `maxDelay`     | The maximum delay between attempts in milliseconds. | 30 seconds      |\n| `timeout`      | The time in milliseconds after which to give up.    | 30 seconds      |\n| `next`         | Chain another policy after the default ones.        | `undefined`     |\n\n#### Retry indefinitely\n\n```javascript\nimport { retryyy } from 'retryyy'\n\nretryyy(\n  async () =\u003e {\n    // do stuff...\n  },\n  {\n    maxAttempts: Infinity,\n    timeout: Infinity,\n  },\n)\n```\n\n#### Disable logs\n\n```javascript\nimport { retryyy } from 'retryyy'\n\nretryyy(\n  async () =\u003e {\n    // do stuff...\n  },\n  {\n    logError: false,\n    logWarn: false,\n  },\n)\n```\n\n### Wrapping functions\n\nWhile `retryyy()` is a handy option, the `wrap()` API allows for better composition and cleaner code by taking existing functions and creating new ones with retry logic attached to them. Its signature is similar but, instead of executing the passed function immediately, it returns a new function.\n\n```typescript\nimport { wrap } from 'retryyy'\n\ntype UserShape = { id: number; name: string }\n\nasync function _fetchUser(id: number) {\n  const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`)\n  return (await res.json()) as UserShape\n}\n\nexport const fetchUser = wrap(_fetchUser, { timeout: 10_000 })\n\nconst user = await fetchUser(1)\nconsole.log(user)\n```\n\n### Wrapping class methods\n\nClass methods can be decorated with `Retryyy` (uppercase initial):\n\n```typescript\nimport { Retryyy } from 'retryyy'\n\ntype UserShape = { id: number; name: string }\n\nclass UserModel {\n  @Retryyy({ timeout: 10_000 })\n  async fetchUser(id: number) {\n    const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`)\n    return (await res.json()) as UserShape\n  }\n}\n\nconst users = new UserModel()\nconst user = await users.fetchUser(1)\nconsole.log(user)\n```\n\n`@Retryyy` decorators use the [Stage 3 ECMAScript Decorators spec](https://github.com/tc39/proposal-decorators) so [TypeScript 5.0](https://devblogs.microsoft.com/typescript/announcing-typescript-5-0/#decorators) or higher is required.\n\nAlternatively, [class field syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Public_class_fields) can be used, but be aware of `this` binding behaviors and the potential performance penalty since the method will be attached to individual instances rather than to the shared prototype.\n\n```typescript\nimport { wrap } from 'retryyy'\n\ntype UserShape = { id: number; name: string }\n\nclass UserModel {\n  fetchUser = wrap(async (id: number) =\u003e {\n    const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`)\n    return (await res.json()) as UserShape\n  })\n}\n```\n\n`@Retryyy` is actually a factory that returns a decorator that can be referenced and applied multiple times:\n\n```typescript\nimport { Retryyy } from 'retryyy'\n\nconst RetryForever = Retryyy({ maxAttempts: Infinity, timeout: Infinity })\n\nclass UserModel {\n  @RetryForever\n  async fetchUser(id: number) {\n    // do stuff...\n  }\n\n  @RetryForever\n  async deleteUser(id: number) {\n    // do stuff...\n  }\n}\n\nclass CartModel {\n  @RetryForever\n  async clearCart() {\n    // do stuff...\n  }\n}\n```\n\n### Custom policies\n\nA policy in `retryyy` is a function that controls the retry behavior based on the current retry state, returning a delay in milliseconds to wait before the next attempt or throwing an error to give up on the operation.\n\n```typescript\nimport type { RetryPolicy } from 'retryyy'\nimport { retryyy } from 'retryyy'\n\nconst customPolicy: RetryPolicy = (state) =\u003e {\n  if (state.attempt \u003e 3 || state.elapsed \u003e 5_000) {\n    throw state.error\n  }\n\n  return state.attempt * 1000\n}\n\ntype UserShape = { id: number; name: string }\n\nretryyy(async () =\u003e {\n  const res = await fetch(`https://jsonplaceholder.typicode.com/users/${1}`)\n  const user = await res.json()\n  console.log(user)\n}, customPolicy)\n```\n\nThis example implements a simple linear backoff, stopping after 3 retries or 5 seconds total, whatever happens first.\n\n### Composing policies\n\nPolicies in `retryyy` can be composed using the `join()` function, allowing to create complex retry strategies from simpler building blocks.\n\n```typescript\nimport type { RetryPolicy } from 'retryyy'\nimport { join, retryyy } from 'retryyy'\n\n/* 1 */\nconst breaker: RetryPolicy = (state, next) =\u003e {\n  if (state.attempt \u003e 5) {\n    throw state.error\n  }\n\n  return next(state)\n}\n\n/* 3 */\nconst jitter: RetryPolicy = (state, next) =\u003e {\n  const delay = next(state)\n  return delay + Math.random() * 1000\n}\n\n/* 2 */\nconst backoff: RetryPolicy = (state) =\u003e {\n  return Math.pow(2, state.attempt - 1) * 1000\n}\n\nconst composedPolicy = join(breaker, jitter, backoff)\n\ntype UserShape = { id: number; name: string }\n\nretryyy(async () =\u003e {\n  const res = await fetch(`https://jsonplaceholder.typicode.com/users/${1}`)\n  const user = await res.json()\n  console.log(user)\n}, composedPolicy)\n```\n\nPolicies are executed left to right, each able to throw an error, return a delay, or call the next policy. This composition allows for flexible and powerful retry strategies tailored to specific needs.\n\nIn this example:\n\n1. `breaker`: Bails out from the operation after 5 attempts.\n2. `backoff`: Exponential backoff starting at 1 second.\n3. `jitter`: Adds some random time to the `delay` returned by the `backoff` (`next`) policy to prevent synchronized retries.\n\nNote that the [`Default` policy](./src/policies/Default.ts) does exactly that.\n\n### Advanced\n\n#### Give up after certain errors\n\n```typescript\nimport { wrap } from 'retryyy'\n\ntype UserShape = { id: number; name: string }\n\n// Typed custom errors might be provided already by the SDKs you are using,\n// but for this example we are creating our own custom error.\nclass APIError extends Error {\n  statusCode: number\n  constructor({ statusCode }: { statusCode: number }) {\n    this.message = 'API responded with an error'\n    this.statusCode = statusCode\n  }\n}\n\nconst _fetchUser = async (id: number) =\u003e {\n  const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`)\n\n  if (!res.ok) {\n    throw new APIError(res)\n  }\n\n  return (await res.json()) as UserShape\n}\n\nexport const fetchUser = wrap(_fetchUser, {\n  next: ({ error }) =\u003e {\n    // Too Many Requests\n    if (error instanceof APIError \u0026\u0026 error.statusCode === 429) {\n      // The server is already rate-limiting us, so bail out as re-trying won't\n      // make any difference.\n      throw error\n    }\n  },\n})\n```\n\n#### Cancel operations mid-flight\n\n[`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) is supported across `retryyy`'s APIs.\n\n```typescript\nimport { retryyy } from 'retryyy'\n\nlet controller: AbortController | null = null\n\nconst handleSubmit = (event: SubmitEvent) =\u003e {\n  event.preventDefault()\n\n  if (controller) {\n    // Do not restart the request if it is already in progress.\n    return\n  }\n\n  try {\n    controller = new AbortController()\n\n    retryyy(\n      async () =\u003e {\n        const res = await fetch(\n          `https://jsonplaceholder.typicode.com/users/1`,\n          { signal: controller?.signal }, // Pass the signal to the fetch call.\n        )\n        const user = await res.json()\n        console.log(user)\n      },\n      {\n        // Pass an empty object if you don't need to customize the default policy.\n      },\n      controller.signal, // Pass the signal to the retryyy call.\n    )\n  } finally {\n    controller = null\n  }\n}\n\nconst handleCancel = (event: MouseEvent) =\u003e {\n  if (controller) {\n    controller.abort(new Error('Request cancelled by the user'))\n  }\n}\n\ndocument.querySelector('form').addEventListener('submit', handleSubmit)\ndocument.querySelector('.cancel-btn').addEventListener('click', handleCancel)\n```\n\nFor functions augmented with `wrap()` or `@Retryyy()`, an `AbortSignal` can be passed as the only argument; a new function will be returned with the signature of the original async function:\n\n```typescript\nimport { wrap } from 'retryyy'\n\n// Move the fetching logic outside.\nconst fetchUser = wrap(async (id: number, signal?: AbortSignal) =\u003e {\n  const res = await fetch(\n    `https://jsonplaceholder.typicode.com/users/${id}`,\n    { signal }, // Pass the signal to the fetch call.\n  )\n  const user = await res.json()\n  // We are only fetching now; let the caller decide what to do with the data.\n  return user as { id: number; name: string }\n})\n\nlet controller: AbortController | null = null\n\nconst handleSubmit = (event: SubmitEvent) =\u003e {\n  event.preventDefault()\n\n  if (controller) {\n    // Do not restart the request if it is already in progress.\n    return\n  }\n\n  try {\n    controller = new AbortController()\n\n    const user = await fetchUser(controller.signal)(1, controller.signal)\n    console.log(user)\n  } finally {\n    controller = null\n  }\n}\n\nconst handleCancel = (event: MouseEvent) =\u003e {\n  if (controller) {\n    controller.abort(new Error('Request cancelled by the user'))\n  }\n}\n\ndocument.querySelector('form').addEventListener('submit', handleSubmit)\ndocument.querySelector('.cancel-btn').addEventListener('click', handleCancel)\n```\n\nIt is important to note that in either case the `AbortSignal` is passed twice: once for `retryyy` to know when to cancel a scheduled attempt and another for the underlying `fetch()` call to cancel the inflight HTTP request.\n\n#### Bandwidth savings\n\nAt only 619 bytes (417B gzipped), the [`core()`](./src/core.ts) implementation is a good option for specific use-cases. Its API is the same as that of `wrap()`, but a policy has to be provided explicitly.\n\n```typescript\nimport { core as wrap } from 'retryyy/core'\nimport type RetryPolicy from 'retryyy/core'\n\nconst simpleExamplePolicy: RetryPolicy = ({ attempt, error }) =\u003e {\n  // Give up after 3 tries.\n  if (attempt \u003e 3) {\n    throw error\n  }\n\n  // Linear backoff, waits 1s, 2s, 3s, 4s, etc.\n  return attempt * 1000\n}\n\nexport const fetchUser = wrap(async (id: number) =\u003e {\n  // do stuff...\n}, simpleExamplePolicy)\n```\n\nIn this case all the retry logic has to be implemented from scratch. For high-throughput production systems it is highly advisable to use a smarter backoff + jitter strategy like the [`PollyJitter` policy](./src/policies/Jitter.ts).\n\n## Motivation\n\nIn the past, I've used various retry libraries like [`node-retry`](https://github.com/tim-kos/node-retry), [`p-retry`](https://github.com/sindresorhus/p-retry), and [`async-retry`](https://github.com/vercel/async-retry), but I've always felt at odds with them.\n\nThe thing that bothers me the most about existing retry libraries is that they force you to write code in a certain way. Retries are primarily an infrastructure reliability concern and rarely part of your core business logic, so it's best to keep them apart.\n\nMoreover, existing libraries often lack the flexibility to customize retry logic to, for example, applying a different jitter strategy.\n\nLately, I've been simply hand-rolling my own retry function when needed:\n\n```javascript\nconst wait = (ms) =\u003e\n  new Promise((resolve, reject) =\u003e {\n    setTimeout(resolve, ms)\n  })\n\nexport const retry = (fn, policy) =\u003e {\n  return async (...args) =\u003e {\n    const state = {\n      attempt: 0,\n      elapsed: 0,\n      error: null,\n      start: Date.now(),\n    }\n\n    while (true) {\n      try {\n        return await fn(...args)\n      } catch (error) {\n        state.attempt += 1\n        state.elapsed = Date.now() - state.start\n        state.error = error\n        await wait(policy(state))\n      }\n    }\n  }\n}\n```\n\nSuch small function is pretty much the entirety of `retryyy`'s [core implementation](./src/core.ts).\n\n## Contributing\n\nPlease refer to [CONTRIBUTING.md](./.github/CONTRIBUTING.md).\n\n## Acknowledgements\n\nThanks to the inspiration from projects like [`node-retry`](https://github.com/tim-kos/node-retry), [`p-retry`](https://github.com/sindresorhus/p-retry), [`async-retry`](https://github.com/vercel/async-retry), and [`cockatiel`](https://github.com/connor4312/cockatiel).\n\nSpecial thanks to the [Polly community](https://www.pollydocs.org/) and [@george-polevoy](https://github.com/george-polevoy) for their [better exponential backoff with jitter](https://github.com/App-vNext/Polly/issues/530).\n\n\u003e 💙 This package was templated with [`create-typescript-app`](https://github.com/JoshuaKGoldberg/create-typescript-app).\n\n## License\n\n[MIT](./LICENSE.md) ❤️\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstefanmaric%2Fretryyy","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fstefanmaric%2Fretryyy","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstefanmaric%2Fretryyy/lists"}