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

  1. Pass a finite value >= 1 (use 1 to disable retries: retryFetch({ attempts: 1 })).
  2. Sanitize config before calling: const attempts = Number.isFinite(n) && n >= 1 ? Math.floor(n) : 3.
  3. Default missing env/config values explicitly (parseInt(x ?? '3', 10) with a NaN fallback).
  4. 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

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


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)