{"record":{"id":"dcc0e7bbf0a289ac","repo":"OpenHands/OpenHands","slug":"retry-attempts-exhausted","errorCode":null,"errorMessage":"Retry attempts exhausted","messagePattern":"Retry attempts exhausted","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/api/settings-service/settings-service.api.ts","lineNumber":155,"sourceCode":"  baseDelayMs: number = 500,\n): Promise<T> {\n  for (let attempt = 0; attempt < maxRetries; attempt += 1) {\n    try {\n      return await fn();\n    } catch (error) {\n      if (attempt >= maxRetries - 1) {\n        throw error;\n      }\n\n      const delay = baseDelayMs * 2 ** attempt;\n\n      await new Promise<void>((resolve) => {\n        setTimeout(resolve, delay);\n      });\n    }\n  }\n\n  throw new Error(\"Retry attempts exhausted\");\n}\n\n/**\n * In-memory cache for settings to avoid repeated network calls.\n * The cache is invalidated on save operations.\n */\nlet settingsCache: {\n  /** Settings with redacted secrets for display */\n  redacted: SettingsApiResponse | null;\n  /** Settings with encrypted secrets for conversation start */\n  encrypted: SettingsApiResponse | null;\n  /** Timestamp when the cache was last populated */\n  timestamp: number;\n} = {\n  redacted: null,\n  encrypted: null,\n  timestamp: 0,\n};","sourceCodeStart":137,"sourceCodeEnd":173,"githubUrl":"https://github.com/OpenHands/OpenHands/blob/500b4c533e9393e44cb92894bcbb18438ef473b6/src/api/settings-service/settings-service.api.ts#L137-L173","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Do not pass maxRetries=0 to withRetry; if you want zero retries, call the function directly without the wrapper.","If you need a 'try once' semantic, use maxRetries=1 so the loop executes once and returns or throws the real error.","Treat this error as a code smell: it indicates a caller misusing the retry helper."],"exampleFix":"// before (triggers the dead-code throw)\nconst result = await withRetry(() => fetchSettings(), 0);\n// after (call directly if no retry is needed)\nconst result = await fetchSettings();","handlingStrategy":"validation","validationCode":"// Ensure maxRetries is always positive before calling withRetry\nfunction safeRetry<T>(fn: () => Promise<T>, retries = 3): Promise<T> {\n  if (retries < 1) return fn(); // single attempt, no loop\n  return withRetry(fn, retries);\n}","typeGuard":null,"tryCatchPattern":"// This error is effectively unreachable with positive maxRetries.\n// If you encounter it, fix the caller — do not catch it.\ntry {\n  await withRetry(() => fetchSettings(), 3);\n} catch (error) {\n  // The real error from fn() propagates; 'Retry attempts exhausted' only\n  // appears if maxRetries was <= 0, which is a caller bug.\n  throw error;\n}","preventionTips":["Never pass maxRetries <= 0 to withRetry; call fn() directly instead.","Audit all withRetry call sites for hardcoded maxRetries values.","Consider adding a runtime assertion at the top of withRetry: if (maxRetries < 1) throw new RangeError('maxRetries must be >= 1')."],"tags":["retry","dead-code","settings-service","unreachable"],"backgroundTag":null,"analyzedSha":"500b4c533e9393e44cb92894bcbb18438ef473b6","analyzedAt":"2026-08-12T10:07:46.034Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}