OpenHands/OpenHands · error · Error

Retry attempts exhausted

Error message

Retry attempts exhausted

What it means

Thrown by the local withRetry() helper inside settings-service.api.ts as a TypeScript-satisfying fallback after the for-loop. In practice this line is unreachable: the loop always either returns on success (attempt < maxRetries) or re-throws the last caught error (attempt >= maxRetries - 1). The throw exists solely so the compiler sees a guaranteed return path after the loop. It would only fire if maxRetries were set to 0 or negative, which the default parameter (3) prevents.

Source

Thrown at src/api/settings-service/settings-service.api.ts:155

  baseDelayMs: number = 500,
): Promise<T> {
  for (let attempt = 0; attempt < maxRetries; attempt += 1) {
    try {
      return await fn();
    } catch (error) {
      if (attempt >= maxRetries - 1) {
        throw error;
      }

      const delay = baseDelayMs * 2 ** attempt;

      await new Promise<void>((resolve) => {
        setTimeout(resolve, delay);
      });
    }
  }

  throw new Error("Retry attempts exhausted");
}

/**
 * In-memory cache for settings to avoid repeated network calls.
 * The cache is invalidated on save operations.
 */
let settingsCache: {
  /** Settings with redacted secrets for display */
  redacted: SettingsApiResponse | null;
  /** Settings with encrypted secrets for conversation start */
  encrypted: SettingsApiResponse | null;
  /** Timestamp when the cache was last populated */
  timestamp: number;
} = {
  redacted: null,
  encrypted: null,
  timestamp: 0,
};

View on GitHub (pinned to 500b4c533e)

Solutions

  1. Do not pass maxRetries=0 to withRetry; if you want zero retries, call the function directly without the wrapper.
  2. If you need a 'try once' semantic, use maxRetries=1 so the loop executes once and returns or throws the real error.
  3. Treat this error as a code smell: it indicates a caller misusing the retry helper.

Example fix

// before (triggers the dead-code throw)
const result = await withRetry(() => fetchSettings(), 0);
// after (call directly if no retry is needed)
const result = await fetchSettings();
Defensive patterns

Strategy: validation

Validate before calling

// Ensure maxRetries is always positive before calling withRetry
function safeRetry<T>(fn: () => Promise<T>, retries = 3): Promise<T> {
  if (retries < 1) return fn(); // single attempt, no loop
  return withRetry(fn, retries);
}

Try / catch

// This error is effectively unreachable with positive maxRetries.
// If you encounter it, fix the caller — do not catch it.
try {
  await withRetry(() => fetchSettings(), 3);
} catch (error) {
  // The real error from fn() propagates; 'Retry attempts exhausted' only
  // appears if maxRetries was <= 0, which is a caller bug.
  throw error;
}

Prevention

When it happens

Trigger: Only reachable if withRetry is called with maxRetries <= 0, causing the for-loop body to never execute (attempt=0 < 0 is false immediately). With the default of 3 retries, this code path is dead. No standard call site in the codebase passes maxRetries=0.

Common situations: A developer passes maxRetries=0 to disable retries but does not realize the function then throws this unreachable sentinel instead of calling fn() at all. This is a logic error in the caller, not a runtime condition.

Related errors


AI-assisted analysis of OpenHands/OpenHands@500b4c533e (2026-08-12). Data as JSON: /api/errors/dcc0e7bbf0a289ac. Report an issue: GitHub.