tursodatabase/turso · error · Error
retryFetch: attempts must be a finite integer >= 1, got
Error message
retryFetch: attempts must be a finite integer >= 1, got ${attempts} What it means
retryFetch (run.ts:159) validates its configuration eagerly: attempts (default 3) must be Number.isFinite and >= 1, otherwise it throws immediately when retryFetch() is called - not per request. It guards the retry loop bounds before any network traffic; note the check enforces finiteness and minimum, so NaN, 0, negative values and Infinity all throw, while fractional values like 2.5 pass despite the 'integer' wording.
Solutions
- Pass a finite value >= 1 (use 1 to disable retries: retryFetch({ attempts: 1 })).
- Sanitize config before calling: const attempts = Number.isFinite(n) && n >= 1 ? Math.floor(n) : 3.
- Default missing env/config values explicitly (parseInt(x ?? '3', 10) with a NaN fallback).
- Remember Infinity is not supported - pick a bounded number for 'many' retries.
Example fix
// before
const attempts = Number(process.env.SYNC_RETRIES); // '' -> 0, undefined -> NaN
const fetchImpl = retryFetch({ attempts }); // throws
// after
const parsed = Number(process.env.SYNC_RETRIES);
const attempts = Number.isFinite(parsed) && parsed >= 1 ? Math.floor(parsed) : 3;
const fetchImpl = retryFetch({ attempts }); Defensive patterns
Strategy: validation
Validate before calling
const raw = Number(process.env.SYNC_RETRIES);
const attempts = Number.isFinite(raw) && raw >= 1 ? Math.floor(raw) : 3;
const fetchImpl = retryFetch({ attempts }); Type guard
const isValidRetryAttempts = (v: unknown): v is number => typeof v === "number" && Number.isFinite(v) && v >= 1;
Prevention
- Use attempts: 1 to disable retries; 0/Infinity/NaN are rejected.
- Sanitize config-derived counts (env vars, YAML) with a finite/integer check and default.
- Construct retryFetch lazily (not at module load) so a bad config fails with a clear stack.
- Floor fractional values before passing them.
When it happens
Trigger: retryFetch({ attempts: 0 }) from a 'disable retries' config; attempts: NaN via a typo'd or unset env variable parsed with parseInt (NaN); attempts: Infinity meant as 'retry forever' (unsupported); negative attempts from arithmetic like userProvided - 1; constructing the fetch at module load, so the throw happens at import time.
Common situations: Config-driven retry counts (env vars, YAML) flowing in unparsed or empty (Number('') === 0); test setups passing attempts: 0 expecting no retries; spreading user options into retryFetch without validating; CI config where a missing variable yields NaN.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- HTTP request failed: . URL: , Method: , Body size: bytes
- HTTP request missing URL: no URL in request and no baseUrl…
- invalid config: url is required
- overwriting the 'Host' header is not supported
- overwriting the 'Host' header is not supported
AI-assisted analysis of tursodatabase/turso@6c72522679 (2026-08-20).
Data as JSON: /api/errors/c8302611b3555186.
Report an issue: GitHub.
Appendix: source
Thrown at bindings/javascript/sync/packages/common/run.ts:159
* ```ts
* import { connect } from '@tursodatabase/sync';
* import { retryFetch } from '@tursodatabase/sync-common';
*
* const db = await connect({
* path: 'local.db',
* url: 'libsql://...',
* fetch: retryFetch(), // defaults
* // fetch: retryFetch({ attempts: 5, delayMs: 1000 }),
* });
* ```
*/
export function retryFetch(opts: RetryFetchOpts = {}): typeof fetch {
const attempts = opts.attempts ?? 3;
const baseDelay = opts.delayMs ?? 500;
const backoff = opts.backoff ?? 2;
const underlying: typeof fetch = opts.fetch ?? ((input, init) => fetch(input, init));
if (!Number.isFinite(attempts) || attempts < 1) {
throw new Error(`retryFetch: attempts must be a finite integer >= 1, got ${attempts}`);
}
return async (input: RequestInfo | URL, init?: RequestInit) => {
let lastError: unknown = null;
let lastResponse: Response | null = null;
let delay = baseDelay;
for (let i = 0; i < attempts; i++) {
try {
const response = await underlying(input, init);
if (response.status < 500 && response.status !== 429) {
return response;
}
lastResponse = response;
lastError = null;
} catch (error) {
lastError = error;
lastResponse = null;
}
if (i + 1 < attempts) {View on GitHub (pinned to 6c72522679)