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
- 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.
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
- 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').
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.