tursodatabase/turso · error · Error
retryFetch: attempts must be a finite integer >= 1, got ${at
Error message
retryFetch: attempts must be a finite integer >= 1, got ${attempts} What it means
retryFetch() validates its options eagerly when the wrapped fetch function is created, and throws this message when attempts is not a finite number or is below 1. It is a configuration-time error: nothing has been fetched yet. Note the message says integer while the check is Number.isFinite(attempts) && attempts >= 1, so non-integer values pass — the failure is specifically NaN, +/-Infinity, 0, or negatives.
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 bad083fafb)
Solutions
- Pass an integer >= 1, e.g. retryFetch({ attempts: 3 }).
- Sanitize config-derived values: const attempts = Number.isFinite(n) && n >= 1 ? Math.floor(n) : 3.
- Use 1 (single attempt, no retries) rather than 0 to disable retrying.
- Check for typos in the option name — attempt instead of attempts silently falls back to the default 3, so a 0/NaN elsewhere is usually the culprit.
Example fix
// before
const attempts = Number(process.env.SYNC_RETRIES); // NaN or 0 when unset/misformatted
const fetchWithRetry = retryFetch({ attempts }); // throws
// after
const raw = Number(process.env.SYNC_RETRIES);
const attempts = Number.isFinite(raw) && raw >= 1 ? Math.floor(raw) : 3;
const fetchWithRetry = retryFetch({ attempts }); Defensive patterns
Strategy: validation
Validate before calling
const raw = Number(config.retries);
const attempts = Number.isFinite(raw) && raw >= 1 ? Math.floor(raw) : 3;
const fetchWithRetry = retryFetch({ attempts }); Type guard
const isValidRetryAttempts = (n: unknown): n is number => typeof n === 'number' && Number.isFinite(n) && n >= 1;
Prevention
- Validate env-derived retry counts before passing them to retryFetch().
- Use 1, not 0, to disable retries.
- Floor fractional values so config like 2.5 does not surprise you later.
When it happens
Trigger: retryFetch({ attempts: 0 }), { attempts: -1 }, or { attempts: NaN }; a retries value parsed from an environment variable that is empty or non-numeric (Number('') === 0, Number('abc') === NaN); a config default of 0 copied from another tool; Infinity from parseInt of a huge string.
Common situations: RETRY_ATTEMPTS env var unset in one environment so the derived value becomes NaN/0; config schemas that allow 0 to mean 'no retries'; spreads of partial option objects where attempts is computed as undefined - 1.
Related errors
- overwriting the 'Host' header is not supported
- overwriting the 'Host' header is not supported
- reader is null
- remoteWritesExperimental requires a non-null URL
- sync is disabled as database was opened without sync support
AI-assisted analysis of tursodatabase/turso@bad083fafb (2026-08-16).
Data as JSON: /api/errors/c8302611b3555186.
Report an issue: GitHub.