{"record":{"id":"66d2f1bf304f81a3","repo":"vercel/ai","slug":"failed-after-trynumber-attempts-with-non-retrya","errorCode":null,"errorMessage":"Failed after ${tryNumber} attempts with non-retryable error: '${errorMessage}'","messagePattern":"Failed after (.+?) attempts with non-retryable error: '(.+?)'","errorType":"exception","errorClass":"RetryError","httpStatus":null,"severity":"error","filePath":"packages/provider-utils/src/retry-with-exponential-backoff.ts","lineNumber":137,"sourceCode":"        f,\n        {\n          maxRetries,\n          delayInMs: backoffFactor * delayInMs,\n          backoffFactor,\n          abortSignal,\n          shouldRetry,\n          getDelayInMs,\n          createRetryError,\n        },\n        newErrors,\n      );\n    }\n\n    if (tryNumber === 1) {\n      throw error; // don't wrap the error when a non-retryable error occurs on the first try\n    }\n\n    throw createRetryError({\n      message: `Failed after ${tryNumber} attempts with non-retryable error: '${errorMessage}'`,\n      reason: 'errorNotRetryable',\n      errors: newErrors,\n    });\n  }\n}\n","sourceCodeStart":119,"sourceCodeEnd":144,"githubUrl":"https://github.com/vercel/ai/blob/69428b1f8b037e4d118fb4853428d5c4e620493c/packages/provider-utils/src/retry-with-exponential-backoff.ts#L119-L144","documentation":"When an operation wrapped by retryWithExponentialBackoff throws an error that shouldRetry classifies as non-retryable, the helper stops immediately and throws a retry error with reason 'errorNotRetryable'. It only wraps when more than one attempt has been made; a non-retryable error on the first attempt is rethrown as-is. The message reports the attempt count and the underlying error message.","triggerScenarios":"f() fails at least once (retried), and then a later attempt throws an error for which shouldRetry returns false (e.g. APICallError with isRetryable === false, like 400/401/403 responses). tryNumber >= 2 at that point produces this wrapped error.","commonSituations":"Expired or invalid API key surfacing mid-run after an initial transient failure; provider returning 400 invalid-request after a retry; hitting a hard quota/non-retryable limit; custom shouldRetry predicates that are too strict and classify recoverable errors as terminal.","solutions":["Inspect the quoted error in the message (and the errors array on the thrown error) to find the non-retryable cause, then fix it (credentials, request body, quota).","If the error should actually be retried, adjust shouldRetry to return true for that error class (e.g. specific status codes).","Reduce unnecessary retries by making the first attempt correct — validate requests before calling so the first failure doesn't lead into a wrapped terminal error.","For programmatic handling, use the retry error's reason === 'errorNotRetryable' to branch (do not retry again) versus 'maxRetriesExceeded'."],"exampleFix":"// before: 401 retried then wrapped\nshouldRetry: ({ error }) => true,\n// after: stop immediately on auth errors\nshouldRetry: ({ error }) => !(error as any)?.isRetryable === false && (error as any)?.statusCode !== 401 || (error as any)?.statusCode === 429 || (error as any)?.statusCode >= 500","handlingStrategy":"try-catch","validationCode":"// fail fast before retrying on obviously non-retryable requests\nfunction assertRetryableSetup(apiKey: string, body: unknown) {\n  if (!apiKey) throw new Error('missing API key: would produce non-retryable 401 after retries');\n  if (body == null) throw new Error('empty request body: would produce non-retryable 400 after retries');\n}","typeGuard":"function isNonRetryableRetryError(e: unknown): e is Error & { reason: 'errorNotRetryable'; errors: unknown[] } {\n  return e instanceof Error && (e as any).reason === 'errorNotRetryable' && Array.isArray((e as any).errors);\n}","tryCatchPattern":"try {\n  await retryFn(() => call());\n} catch (error) {\n  if ((error as any)?.reason === 'errorNotRetryable') {\n    // do NOT retry again; surface or fix the underlying error\n    const root = (error as any).errors.at(-1);\n    throw root ?? error;\n  }\n  throw error;\n}","preventionTips":["Classify errors in shouldRetry accurately using APICallError.isRetryable / statusCode.","Never blanket-return true from shouldRetry; auth and validation errors will never succeed on retry.","Check the quoted last error in the message to identify the terminal cause.","For first-attempt failures, the original error is thrown unwrapped — handle it directly."],"tags":["retry","non-retryable","api-error","network"],"backgroundTag":"non-retryable-error","analyzedSha":"69428b1f8b037e4d118fb4853428d5c4e620493c","analyzedAt":"2026-08-30T12:32:21.016Z","schemaVersion":2},"datasetVersion":"2026-08-30T13:17:10.514Z"}