{"record":{"id":"c3b7a3dc2ceb6187","repo":"vercel/ai","slug":"failed-after-trynumber-attempts-last-error","errorCode":null,"errorMessage":"Failed after ${tryNumber} attempts. Last error: ${errorMessage}","messagePattern":"Failed after (.+?) attempts\\. Last error: (.+?)","errorType":"exception","errorClass":"RetryError","httpStatus":null,"severity":"error","filePath":"packages/provider-utils/src/retry-with-exponential-backoff.ts","lineNumber":102,"sourceCode":"  errors: unknown[] = [],\n): Promise<OUTPUT> {\n  try {\n    return await f();\n  } catch (error) {\n    if (isAbortError(error)) {\n      throw error; // don't retry when the request was aborted\n    }\n\n    if (maxRetries === 0) {\n      throw error; // don't wrap the error when retries are disabled\n    }\n\n    const errorMessage = getErrorMessage(error);\n    const newErrors = [...errors, error];\n    const tryNumber = newErrors.length;\n\n    if (tryNumber > maxRetries) {\n      throw createRetryError({\n        message: `Failed after ${tryNumber} attempts. Last error: ${errorMessage}`,\n        reason: 'maxRetriesExceeded',\n        errors: newErrors,\n      });\n    }\n\n    if ((await shouldRetry(error)) && tryNumber <= maxRetries) {\n      await delay(\n        getDelayInMs({\n          error,\n          exponentialBackoffDelay: delayInMs,\n        }),\n        { abortSignal },\n      );\n\n      return retryWithExponentialBackoffInternal(\n        f,\n        {","sourceCodeStart":84,"sourceCodeEnd":120,"githubUrl":"https://github.com/vercel/ai/blob/69428b1f8b037e4d118fb4853428d5c4e620493c/packages/provider-utils/src/retry-with-exponential-backoff.ts#L84-L120","documentation":"The SDK's retry helper wraps an operation with exponential backoff. When every retry attempt fails and the number of recorded failures (tryNumber) exceeds maxRetries, it gives up and throws a retry error with reason 'maxRetriesExceeded'. The message embeds the total attempt count and the message of the last underlying error, so the real failure cause is preserved as the 'last error' text (and the original errors array is attached to the created error).","triggerScenarios":"A call wrapped by retryWithExponentialBackoff (used internally by provider fetches when maxRetries > 0) throws on every attempt, and each failure passes the shouldRetry predicate, until newErrors.length > maxRetries. E.g. maxRetries: 2 means attempts 1,2 are retried and the error is thrown once tryNumber reaches 3.","commonSituations":"Provider API outage or sustained 429/5xx responses; invalid API key causing repeated 401s where shouldRetry still allows retrying; network connectivity loss; misconfigured maxRetries being high while the endpoint is permanently failing; request payload rejected (400) by a provider with a retryable-classification bug.","solutions":["Read the 'Last error: ...' portion of the message and the errors array on the thrown retry error to find the root cause, then fix that underlying error first.","If the root cause is rate limiting, reduce request concurrency or add backoff-friendly getDelayInMs and increase maxRetries.","If the root cause is auth or bad request, fix credentials/request before retrying; also tighten shouldRetry so non-retryable errors (e.g. 401/400) fail fast.","If the operation is expected to fail and wrapping obscures it, set maxRetries: 0 so the original error is thrown unwrapped."],"exampleFix":"// before: opaque wrapped failure\nconst result = await retryWithExponentialBackoff({ maxRetries: 5, shouldRetry })(() => fetch(url));\n// after: fail fast on non-retryable statuses and log root cause\nconst result = await retryWithExponentialBackoff({\n  maxRetries: 3,\n  shouldRetry: ({ error }) => error?.statusCode == null || error.statusCode >= 500 || error.statusCode === 429,\n})(() => fetch(url));","handlingStrategy":"try-catch","validationCode":"// check retry budget vs expected flakiness before wrapping\nif (maxRetries < 1) throw new Error('maxRetries must be >= 1 to use retryWithExponentialBackoff');","typeGuard":null,"tryCatchPattern":"try {\n  return await retryFn(() => call());\n} catch (error) {\n  // inspect last error text / attached errors array\n  const causes = (error as any)?.errors ?? [];\n  console.error('All attempts failed:', error.message, causes);\n  if (isAbortError(error)) throw error; // aborted calls are rethrown unwrapped\n  throw error;\n}","preventionTips":["Set shouldRetry to only allow retryable status codes (429, 5xx, network errors) so terminal failures surface fast.","Size maxRetries and getDelayInMs to the provider's rate-limit window.","Log the full errors array from the wrapped retry error, not just its top message.","Validate API keys and request payloads before the first attempt."],"tags":["retry","exponential-backoff","rate-limit","network"],"backgroundTag":"max-retries-exceeded","analyzedSha":"69428b1f8b037e4d118fb4853428d5c4e620493c","analyzedAt":"2026-08-30T12:32:21.016Z","schemaVersion":2},"datasetVersion":"2026-08-30T13:17:10.514Z"}