vercel/ai · error · RetryError
Failed after ${tryNumber} attempts with non-retryable error:
Error message
Failed after ${tryNumber} attempts with non-retryable error: '${errorMessage}' What it means
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.
Source
Thrown at packages/provider-utils/src/retry-with-exponential-backoff.ts:137
f,
{
maxRetries,
delayInMs: backoffFactor * delayInMs,
backoffFactor,
abortSignal,
shouldRetry,
getDelayInMs,
createRetryError,
},
newErrors,
);
}
if (tryNumber === 1) {
throw error; // don't wrap the error when a non-retryable error occurs on the first try
}
throw createRetryError({
message: `Failed after ${tryNumber} attempts with non-retryable error: '${errorMessage}'`,
reason: 'errorNotRetryable',
errors: newErrors,
});
}
}
View on GitHub (pinned to 69428b1f8b)
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'.
Example fix
// before: 401 retried then wrapped
shouldRetry: ({ error }) => true,
// after: stop immediately on auth errors
shouldRetry: ({ error }) => !(error as any)?.isRetryable === false && (error as any)?.statusCode !== 401 || (error as any)?.statusCode === 429 || (error as any)?.statusCode >= 500 Defensive patterns
Strategy: try-catch
Validate before calling
// fail fast before retrying on obviously non-retryable requests
function assertRetryableSetup(apiKey: string, body: unknown) {
if (!apiKey) throw new Error('missing API key: would produce non-retryable 401 after retries');
if (body == null) throw new Error('empty request body: would produce non-retryable 400 after retries');
} Type guard
function isNonRetryableRetryError(e: unknown): e is Error & { reason: 'errorNotRetryable'; errors: unknown[] } {
return e instanceof Error && (e as any).reason === 'errorNotRetryable' && Array.isArray((e as any).errors);
} Try / catch
try {
await retryFn(() => call());
} catch (error) {
if ((error as any)?.reason === 'errorNotRetryable') {
// do NOT retry again; surface or fix the underlying error
const root = (error as any).errors.at(-1);
throw root ?? error;
}
throw error;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- TranscriptionJobSubmissionFailed
- Failed to reconnect after ${this.maxConsecutiveErrors} conse
- Cannot connect to API: ${cause.message}
- Failed after ${tryNumber} attempts. Last error: ${errorMessa
- Video generation timed out after ${timeoutMs}ms.
AI-assisted analysis of vercel/ai@69428b1f8b (2026-08-30).
Data as JSON: /api/errors/66d2f1bf304f81a3.
Report an issue: GitHub.