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

  1. 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).
  2. If the error should actually be retried, adjust shouldRetry to return true for that error class (e.g. specific status codes).
  3. 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.
  4. 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

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


AI-assisted analysis of vercel/ai@69428b1f8b (2026-08-30). Data as JSON: /api/errors/66d2f1bf304f81a3. Report an issue: GitHub.