Errors
The AiError type, its seven reasons, and how to handle and retry them.
A failed call fails its Effect with an AiError (tag ai-effect/AiError), instead of the SDK’s thrown errors. It has:
method: the function that failed:'generateText','streamText','embed','embedMany','rerank','ToolLoopAgent.generate','ToolLoopAgent.stream','createUIMessageStream'or'createAgentUIStream'reason: one of the seven reasons belowmessageandisRetryable, both derived from the reason
Reasons
reason._tag |
What maps to it | isRetryable |
|---|---|---|
ai-effect/RateLimitError |
HTTP 429; retryAfter is a Duration parsed from the retry-after header when present |
yes |
ai-effect/AuthenticationError |
HTTP 401 or 403 | no |
ai-effect/InvalidRequestError |
HTTP 400, 404, 409 or 422, or a bad prompt, argument, model or unsupported feature | no |
ai-effect/ProviderError |
HTTP 5xx, or a network failure with no status | the SDK’s own isRetryable |
ai-effect/InvalidOutputError |
the model returned no output, or output that failed parsing, validation or tool lookup | yes |
ai-effect/AbortedError |
the call was aborted, or timed out (see below) | no |
ai-effect/UnknownError |
anything else, including errors thrown by your own code and other HTTP statuses | no |
Every reason has a message, a cause holding the raw SDK error, and a statusCode when the provider returned one. If the SDK retried and gave up, cause is the last underlying error rather than the SDK’s retry wrapper.
Aborts
AbortedError covers a call stopped by an abortSignal you passed, an SDK timeout, or a provider abort. A stream that ends with an abort part fails with it too. It has no statusCode.
Interrupting the fiber is not an AbortedError: it stays an interrupt and never reaches the error channel.
Handle one reason
Effect.catchReason handles a single reason and leaves the others in the error channel:
import { generateText } from 'ai-effect';
import { Effect } from 'effect';
const reply = generateText({
model: 'openai/gpt-5',
prompt: 'Hello!',
}).pipe(
Effect.map((result) => result.text),
Effect.catchReason(
'ai-effect/AiError',
'ai-effect/RateLimitError',
(reason) =>
Effect.logWarning('rate limited', reason.retryAfter).pipe(
Effect.as('Fallback response'),
),
),
);
Retry
Drive a retry policy with isRetryable:
import { generateText } from 'ai-effect';
import { Effect } from 'effect';
const resilient = generateText({
model: 'openai/gpt-5',
prompt: 'Hello!',
}).pipe(
Effect.retry({
while: (error) => error.isRetryable,
times: 3,
}),
);
The AI SDK also retries on its own (its maxRetries option); Effect.retry applies after those attempts are exhausted.
Streaming errors
streamText and ToolLoopAgent.stream return their result as soon as the stream is set up, so the returned Effect only fails for errors thrown at that point. Errors during streaming fail the stream you consume, or the field Effect you await, such as result.text, with an AiError that has the same method. The result’s stream field is the exception: it emits error and abort parts as data.
Tool failures
A failing tool execute does not produce an AiError, including inside a ToolLoopAgent; see Tools.