https://github.com/didrod205/fetchwise
Tiny zero-dependency resilient fetch — retries, exponential backoff, timeouts & Retry-After. ~1KB, works everywhere fetch does.
https://github.com/didrod205/fetchwise
backoff exponential-backoff fetch fetch-retry http retries retry timeout typescript zero-dependency
Last synced: about 20 hours ago
JSON representation
Tiny zero-dependency resilient fetch — retries, exponential backoff, timeouts & Retry-After. ~1KB, works everywhere fetch does.
- Host: GitHub
- URL: https://github.com/didrod205/fetchwise
- Owner: didrod205
- License: mit
- Created: 2026-05-29T06:21:48.000Z (19 days ago)
- Default Branch: main
- Last Pushed: 2026-06-02T03:38:53.000Z (15 days ago)
- Last Synced: 2026-06-02T05:17:43.633Z (15 days ago)
- Topics: backoff, exponential-backoff, fetch, fetch-retry, http, retries, retry, timeout, typescript, zero-dependency
- Language: TypeScript
- Homepage: https://www.npmjs.com/package/fetchwise
- Size: 45.9 KB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Contributing: CONTRIBUTING.md
- Funding: .github/FUNDING.yml
- License: LICENSE
- Code of conduct: CODE_OF_CONDUCT.md
Awesome Lists containing this project
README
# fetchwise
**A tiny, zero-dependency resilient `fetch` — retries, exponential backoff, timeouts and `Retry-After`, in ~1 KB gzipped.**
[](https://www.npmjs.com/package/fetchwise)
[](https://bundlephobia.com/package/fetchwise)
[](https://github.com/didrod205/fetchwise/actions/workflows/ci.yml)
[](https://www.npmjs.com/package/fetchwise)
[](./LICENSE)
`fetchwise` wraps the **native `fetch`** you already use and makes it survive flaky
networks and overloaded servers — without changing the API you know.
```ts
import { fetchwise } from "fetchwise";
// Exactly like fetch, but it retries transient failures automatically.
const res = await fetchwise("https://api.example.com/users");
const users = await res.json();
```
---
## Why fetchwise?
- 🪶 **Zero dependencies.** ~1 KB gzipped. Nothing to audit, nothing to bloat your bundle.
- 🔁 **Smart retries.** Exponential backoff with full jitter, configurable per request.
- ⏱️ **Per-attempt timeouts.** Built on `AbortController` — no hanging requests.
- 🚦 **Respects `Retry-After`.** Honors the server's own backoff hints on `429` / `503`.
- 🌍 **Runs everywhere.** Node 18+, Deno, Bun, Cloudflare Workers and the browser — anywhere `fetch` exists.
- 🧩 **Drop-in.** Same signature as `fetch`. Add resilience by passing one extra `retry` option.
- 🛡️ **Type-safe.** Written in TypeScript, ships full type declarations.
## Install
```bash
npm install fetchwise
# or: pnpm add fetchwise / yarn add fetchwise / bun add fetchwise
```
No build step needed — it ships ESM **and** CommonJS.
```ts
import { fetchwise } from "fetchwise"; // ESM / TypeScript
const { fetchwise } = require("fetchwise"); // CommonJS
```
## Usage
### Drop-in replacement
Anywhere you call `fetch`, call `fetchwise` instead. By default it makes up to
**4 attempts** (1 + 3 retries) on network errors and on the status codes
`408, 425, 429, 500, 502, 503, 504`.
```ts
const res = await fetchwise("/api/data");
```
### JSON in one call
`fetchJson` retries like `fetchwise`, **throws `HttpError` on a non-2xx** (so you
never parse an error page by mistake), and returns typed JSON. Pass `json` to
serialize a body and set `Content-Type` automatically.
```ts
import { fetchJson, HttpError } from "fetchwise";
const user = await fetchJson("/api/users/1");
const created = await fetchJson("/api/users", { json: { name: "Ada" } });
try {
await fetchJson("/api/secret");
} catch (e) {
if (e instanceof HttpError) console.log(e.status, e.response.url);
}
```
### Tune the retry behavior
```ts
const res = await fetchwise("/api/data", {
method: "POST",
body: JSON.stringify({ hello: "world" }),
headers: { "content-type": "application/json" },
retry: {
retries: 5, // up to 6 attempts total
minDelay: 200, // first backoff (ms)
maxDelay: 10_000, // cap (ms)
timeout: 4_000, // abort & retry any attempt slower than 4s
onRetry: ({ attempt, delay, error, response }) => {
console.warn(`retry #${attempt} in ${delay}ms`, error ?? response?.status);
},
},
});
```
### Create a preconfigured client
Share defaults across your whole app — per-call options are merged on top.
```ts
import { create } from "fetchwise";
const api = create({ retries: 5, timeout: 5_000 });
await api("/users"); // uses the defaults
await api("/report", { retry: { retries: 0 } }); // override per call
```
### Cancel with an AbortSignal
External aborts are respected immediately and are **never** retried.
```ts
const res = await fetchwise(url, { signal: AbortSignal.timeout(10_000) });
```
### Retry only what you want
```ts
await fetchwise(url, {
retry: {
// Custom status policy (array or predicate)
retryOnStatus: (status) => status >= 500,
// Custom error policy — e.g. don't retry DNS failures
retryOnError: (err) => !String(err).includes("ENOTFOUND"),
},
});
```
## API
### `fetchwise(input, init?) => Promise`
Identical to `fetch(input, init)`, plus an optional `init.retry` object.
Returns the final `Response`. Retryable statuses are returned as-is once retries
are exhausted (it does **not** throw on HTTP errors — same as `fetch`).
### `create(defaults?) => fetchwise`
Returns a `fetchwise` function with the given `RetryOptions` baked in.
### `RetryOptions`
| Option | Type | Default | Description |
| ------------------- | ------------------------------------------------- | ------------------------------------ | -------------------------------------------------------- |
| `retries` | `number` | `3` | Additional attempts after the first failure. |
| `minDelay` | `number` | `300` | Base backoff delay in ms. |
| `maxDelay` | `number` | `30000` | Maximum delay between attempts in ms. |
| `factor` | `number` | `2` | Exponential backoff multiplier. |
| `jitter` | `boolean` | `true` | Apply full random jitter to each delay. |
| `timeout` | `number` | `0` (off) | Per-attempt timeout in ms. |
| `retryOnStatus` | `number[] \| (status, response) => boolean` | `[408,425,429,500,502,503,504]` | Which statuses to retry. |
| `retryOnError` | `(error, context) => boolean` | retry all but external aborts | Whether a thrown error is retryable. |
| `respectRetryAfter` | `boolean` | `true` | Honor the `Retry-After` response header. |
| `onRetry` | `(context) => void` | — | Hook fired before each retry. |
### Other exports
- `TimeoutError` — thrown internally when a `timeout` elapses (retried like any error).
- `parseRetryAfter(value, now?)` — parse a `Retry-After` header into milliseconds.
## How backoff works
Each retry waits `min(maxDelay, minDelay × factor^(attempt-1))`, then — with
`jitter` on (the default) — a random value between `0` and that ceiling. Full
jitter spreads out retries from many clients so they don't stampede a recovering
server. If the response carries a `Retry-After` header, that value wins.
## Comparison
| | `fetchwise` | hand-rolled `try/catch` loop | heavier HTTP clients |
| ------------------------ | :---------: | :--------------------------: | :------------------: |
| Zero dependencies | ✅ | ✅ | ❌ |
| Native `fetch` signature | ✅ | ⚠️ | ❌ |
| Backoff + jitter | ✅ | ❌ | ✅ |
| `Retry-After` support | ✅ | ❌ | ⚠️ |
| Per-attempt timeout | ✅ | ❌ | ✅ |
| ~1 KB gzipped | ✅ | — | ❌ |
## Contributing
Contributions are very welcome! Please read [CONTRIBUTING.md](./CONTRIBUTING.md)
and our [Code of Conduct](./CODE_OF_CONDUCT.md). Good first issues are labeled
[`good first issue`](https://github.com/didrod205/fetchwise/labels/good%20first%20issue).
```bash
git clone https://github.com/didrod205/fetchwise.git
cd fetchwise
npm install
npm test
```
## 💖 Sponsor
`fetchwise` is free and MIT-licensed, built and maintained in spare time. If it
saves you from writing yet another retry loop, please consider supporting it —
every bit helps keep the project healthy and the issues answered.
- ⭐ **Star this repo** — the simplest, free way to help others discover it.
- 🍋 **[Sponsor via Lemon Squeezy](https://elab-studio.lemonsqueezy.com/checkout/buy/5d059b89-51d0-456b-b33a-ed56994f7010)** — one-time or recurring support.
> Sponsoring? Open an issue and we'll add your name/logo here. Thank you! 🙏
## License
[MIT](./LICENSE) © fetchwise contributors