dubinc/dub · error

Failed after ${maxRetries} retries. Last error: ${lastError.

Error message

Failed after ${maxRetries} retries. Last error: ${lastError.message}

What it means

fetchWithRetry throws this Error when every one of the maxRetries attempts fails with a retryable condition — network failure, DNS error, abort due to timeout, or repeated 429/5xx responses. The original cause is preserved in the message as `Last error: ...`, so the final thrown error is a wrapper describing exhausted retries plus the last underlying failure.

Source

Thrown at packages/utils/src/functions/fetch-with-retry.ts:64

        let errorMessage: string;
        try {
          const error = await response.json();
          errorMessage = error.error || `HTTP error ${response.status}`;
        } catch {
          errorMessage = `HTTP error ${response.status}`;
        }
        console.error(`fetchWithRetry error: ${errorMessage}`);
        throw new Error(errorMessage);
      }
    } catch (error) {
      clearTimeout(timeoutId);
      lastError = error instanceof Error ? error : new Error(String(error));

      // If this is the last retry, throw the error
      if (i === maxRetries - 1) {
        const errMsg = `Failed after ${maxRetries} retries. Last error: ${lastError.message}`;
        console.error(`fetchWithRetry error: ${errMsg}`);
        throw new Error(errMsg);
      }

      // For network errors or timeouts, wait and retry
      const delay = retryDelay + Math.pow(i, 2) * 50;
      await new Promise((resolve) => setTimeout(resolve, delay));
    }
  }

  // This should never be reached due to the throw in the last retry,
  // but TypeScript needs it for type safety
  throw new Error(`Failed after ${maxRetries} retries`);
}

View on GitHub (pinned to f216b94a24)

Solutions

  1. Read the `Last error:` suffix to identify the root cause (AbortError/timeout vs TypeError/network vs 429/5xx).
  2. If timeouts dominate, raise the `timeout` option (e.g. timeout: 30000) so slow endpoints aren't aborted.
  3. If 429/5xx dominate, increase `maxRetries` and `retryDelay`, or back off globally — the server may need minutes, not seconds.
  4. Check basic connectivity to the host (curl/ping) and any proxy/firewall settings before re-running.
  5. Wrap the call in your own try/catch and fall back to cached data or queue the work for later retry.

Example fix

// before
const res = await fetchWithRetry(url); // default 5s timeout exhausts retries
// after
const res = await fetchWithRetry(url, undefined, { timeout: 30000, maxRetries: 5, retryDelay: 2000 });
Defensive patterns

Strategy: retry

Validate before calling

// Check reachability before the call
const host = new URL(input instanceof URL ? input.href : String(input)).host;
const reachable = await fetch(`https://${host}/`, { method: 'HEAD' }).then(r => r.ok).catch(() => false);
if (!reachable) console.warn('Host unreachable; expect retries to exhaust');

Type guard

function isRetryExhaustionError(e: unknown): e is Error {
  return e instanceof Error && /^Failed after \d+ retries\./.test(e.message);
}

Try / catch

try {
  const res = await fetchWithRetry(url, init, { timeout: 30000, maxRetries: 5, retryDelay: 2000 });
} catch (e) {
  if (isRetryExhaustionError(e)) {
    // queue for later / serve cached data / alert on the underlying cause
  } else {
    throw e;
  }
}

Prevention

When it happens

Trigger: maxRetries consecutive attempts where fetch rejects (offline/DNS failure), the AbortController fires (response slower than `timeout`, default 5000ms), or the server keeps returning 429 or 5xx on every attempt.

Common situations: Calling an API that is down or deploying during an outage, running behind a firewall/VPN blocking the host, a slow endpoint exceeding the 5s default timeout 10 times in a row, or sustained rate limiting (429) that outlasts the backoff schedule.

Related errors


AI-assisted analysis of dubinc/dub@f216b94a24 (2026-08-31). Data as JSON: /api/errors/8ab4743bb2740a44. Report an issue: GitHub.