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

  1. Pass an integer >= 1, e.g. retryFetch({ attempts: 3 }).
  2. Sanitize config-derived values: const attempts = Number.isFinite(n) && n >= 1 ? Math.floor(n) : 3.
  3. Use 1 (single attempt, no retries) rather than 0 to disable retrying.
  4. 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

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


AI-assisted analysis of tursodatabase/turso@bad083fafb (2026-08-16). Data as JSON: /api/errors/c8302611b3555186. Report an issue: GitHub.