AI Errors
Every AI operation in Effect fails with a single, unified error type: AiError. Instead of a sprawling union of provider-specific error classes leaking into your Effect’s error channel, AiError is a thin wrapper that carries:
moduleandmethod— where the failure originated (e.g."OpenAiLanguageModel","generateText").reason— a semantic, tagged error describing what went wrong (rate limited, bad auth, invalid output, a tool failure, …).
This is the reason pattern: one error tag (AiError) in your type signatures, with a rich, discriminated reason inside it. You catch the wrapper once and branch on the reason, or use the dedicated Effect.catchReason helper to narrow straight to a reason.
import { Effect } from "effect"import { LanguageModel } from "effect/unstable/ai"import { AiError } from "effect/unstable/ai"
const program = LanguageModel.generateText({ prompt: "Write a haiku about TypeScript"}).pipe( // generateText fails with AiError.AiError Effect.catchTag("AiError", (error) => Effect.logError(`[${error.module}.${error.method}] ${error.reason._tag}: ${error.message}`) ))The common case: catch, branch, inspect
Section titled “The common case: catch, branch, inspect”Catching by tag, branching on the reason
Section titled “Catching by tag, branching on the reason”Because the error channel only ever contains AiError, Effect.catchTag with "AiError" recovers from all AI failures in one place. Switch on reason._tag to decide what to do:
import { Effect } from "effect"import { LanguageModel, AiError } from "effect/unstable/ai"
const summarize = (text: string) => LanguageModel.generateText({ prompt: text }).pipe( Effect.map((response) => response.text), Effect.catchTag("AiError", (error) => { switch (error.reason._tag) { case "RateLimitError": // retryAfter is surfaced on both the reason and the wrapper return Effect.fail(`Slow down — retry after ${error.retryAfter}`) case "AuthenticationError": return Effect.fail(`Bad credentials (${error.reason.kind})`) case "ContentPolicyError": return Effect.succeed("[redacted — content policy]") default: return Effect.fail(error.message) } }) )Catching a single reason with Effect.catchReason
Section titled “Catching a single reason with Effect.catchReason”When you only care about one reason, Effect.catchReason lets you skip the manual switch and narrow directly. It takes the error tag, the reason tag, a handler that receives the narrowed reason, and an optional fallback for the other reasons:
import { Effect } from "effect"import { LanguageModel } from "effect/unstable/ai"
const resilient = LanguageModel.generateText({ prompt: "Explain backpressure"}).pipe( // `reason` here is narrowed to AiError.RateLimitError Effect.catchReason( "AiError", "RateLimitError", (reason) => // reason.retryAfter is a Duration | undefined Effect.logWarning(`rate limited`).pipe( Effect.zipRight(Effect.fail("rate limited")) ), // all other reasons pass through unchanged (otherReason, error) => Effect.fail(error) ))Retrying transient failures with isRetryable
Section titled “Retrying transient failures with isRetryable”Every reason exposes an isRetryable getter, and the AiError wrapper delegates to it. Combine it with Effect.retry to back off only on transient errors (network blips, 5xx, rate limits, non-deterministic output) and fail fast on permanent ones (bad auth, content policy, malformed requests):
import { Effect, Schedule } from "effect"import { LanguageModel, AiError } from "effect/unstable/ai"
const withRetry = LanguageModel.generateText({ prompt: "Generate a slogan"}).pipe( Effect.retry({ schedule: Schedule.exponential("200 millis"), times: 4, // only retry when the underlying reason is transient while: (error: AiError.AiError) => error.isRetryable }))Reading HTTP context, provider metadata, and usage
Section titled “Reading HTTP context, provider metadata, and usage”Provider-facing reasons attach extra forensic context. Reach into reason to pull it out:
reason.http— anHttpContext(request details, optional response details, raw body) on provider/transport reasons.reason.metadata—ProviderMetadata, namespaced per provider (e.g.metadata.openai).reason.usage—UsageInfo(token counts) on output reasons likeInvalidOutputError.
import { Effect } from "effect"import { LanguageModel } from "effect/unstable/ai"
const logContext = LanguageModel.generateText({ prompt: "Hi" }).pipe( Effect.catchTag("AiError", (error) => { const reason = error.reason // Provider HTTP context (present on most provider reasons) if ("http" in reason && reason.http) { const { request, response } = reason.http Effect.runSync( Effect.logError( `HTTP ${response?.status ?? "?"} for ${request.method} ${request.url}` ) ) } // Provider-specific metadata, keyed by provider name if ("metadata" in reason) { const openai = reason.metadata.openai // MutableJson | null | undefined console.log("openai metadata:", openai) } // Token usage (on output reasons) if ("usage" in reason && reason.usage) { console.log("tokens:", reason.usage.totalTokens) } return Effect.fail(error) }))Reference
Section titled “Reference”All of the following are exported from effect/unstable/ai via the AiError namespace.
AiError
Section titled “AiError”The top-level wrapper error — the only error type AI operations put in their error channel. It carries module, method, and the semantic reason, and delegates isRetryable / retryAfter to that reason. Its message is formatted as "<module>.<method>: <reason message>".
import { Duration } from "effect"import { AiError } from "effect/unstable/ai"
const error = new AiError.AiError({ module: "OpenAi", method: "generateText", reason: new AiError.RateLimitError({ retryAfter: Duration.seconds(60) })})
error._tag // => "AiError"error.isRetryable // => true (delegated to the reason)error.retryAfter // => Duration("60 seconds")error.message // => "OpenAi.generateText: Rate limit exceeded. Retry after 1m"Constructor for an AiError from a module, method, and reason. Equivalent to new AiError.AiError({ ... }); prefer it when constructing errors inside provider integrations.
import { AiError } from "effect/unstable/ai"
const error = AiError.make({ module: "Anthropic", method: "completion", reason: new AiError.QuotaExhaustedError({})})
error.message // => "Anthropic.completion: Quota exhausted. Check your account billing and usage limits."AiErrorReason
Section titled “AiErrorReason”Both a TypeScript union type and a runtime Schema.Union of every concrete reason class below. Use the type to annotate handlers, and the schema to decode/validate an unknown reason value.
import { Schema } from "effect"import { AiError } from "effect/unstable/ai"
// As a typeconst handle = (reason: AiError.AiErrorReason) => reason._tag
// As a schema (decode an unknown reason)const decode = Schema.decodeUnknownSync(AiError.AiErrorReason)AiErrorEncoded
Section titled “AiErrorEncoded”The serialized (JSON-encoded) form of an AiError — useful when sending errors across a wire boundary (RPC, workers) where they are encoded/decoded via Schema.
import { AiError } from "effect/unstable/ai"
type Encoded = AiError.AiErrorEncoded// => { _tag: "AiError"; module: string; method: string; reason: ... }isAiError
Section titled “isAiError”Type guard narrowing an unknown value to AiError. Handy at boundaries where the error type has been widened to unknown.
import { AiError } from "effect/unstable/ai"
const aiError = AiError.make({ module: "Test", method: "x", reason: new AiError.RateLimitError({})})
AiError.isAiError(aiError) // => trueAiError.isAiError(new Error("oops")) // => falseisAiErrorReason
Section titled “isAiErrorReason”Type guard narrowing an unknown value to one of the AiErrorReason classes (i.e. a value carrying the reason brand).
import { AiError } from "effect/unstable/ai"
AiError.isAiErrorReason(new AiError.RateLimitError({})) // => trueAiError.isAiErrorReason(new Error("oops")) // => falsereasonFromHttpStatus
Section titled “reasonFromHttpStatus”Maps an HTTP status code to the appropriate semantic reason. Provider packages use it as a base mapping (400 → InvalidRequestError, 401 → AuthenticationError InvalidKey, 403 → AuthenticationError InsufficientPermissions, 429 → RateLimitError, ≥500 → InternalProviderError, otherwise → UnknownError).
import { AiError } from "effect/unstable/ai"
AiError.reasonFromHttpStatus({ status: 429 })._tag // => "RateLimitError"AiError.reasonFromHttpStatus({ status: 401 })._tag // => "AuthenticationError"AiError.reasonFromHttpStatus({ status: 503 })._tag // => "InternalProviderError"AiError.reasonFromHttpStatus({ status: 418 })._tag // => "UnknownError"Transport & provider reasons
Section titled “Transport & provider reasons”These reasons describe failures coming from (or before reaching) the provider. Most carry optional http context and provider metadata.
NetworkError
Section titled “NetworkError”A transport-level failure that occurred before an HTTP response was received: connectivity loss, request encoding failure, or an invalid URL. Discriminated by reason ("TransportError" is retryable; "EncodeError" and "InvalidUrlError" are not). Build one from a platform HttpClientError.RequestError with NetworkError.fromRequestError.
import { AiError } from "effect/unstable/ai"
const error = new AiError.NetworkError({ reason: "TransportError", request: { method: "POST", url: "https://api.openai.com/v1/completions", urlParams: [], hash: undefined, headers: { "Content-Type": "application/json" } }, description: "Connection timeout after 30 seconds"})
error.isRetryable // => true (only TransportError is retryable)error.message // => "TransportError: Connection timeout after 30 seconds (POST .../v1/completions) ..."RateLimitError
Section titled “RateLimitError”The request was throttled (typically a 429). Always retryable; when the provider supplies a retryAfter Duration, wait that long before retrying. Carries provider metadata and optional http context.
import { Duration } from "effect"import { AiError } from "effect/unstable/ai"
const error = new AiError.RateLimitError({ retryAfter: Duration.seconds(60) })
error.isRetryable // => trueerror.retryAfter // => Duration("60 seconds")error.message // => "Rate limit exceeded. Retry after 1m"QuotaExhaustedError
Section titled “QuotaExhaustedError”Account or billing limits have been reached. Not retryable without user action; an optional resetAt DateTimeUtc indicates when the quota refreshes.
import { AiError } from "effect/unstable/ai"
const error = new AiError.QuotaExhaustedError({})
error.isRetryable // => falseerror.message // => "Quota exhausted. Check your account billing and usage limits."AuthenticationError
Section titled “AuthenticationError”Invalid, expired, missing, or under-privileged credentials. Never retryable without changing credentials. The kind field is one of "InvalidKey", "ExpiredKey", "MissingKey", "InsufficientPermissions", "Unknown" and drives the suggestion in message.
import { AiError } from "effect/unstable/ai"
const error = new AiError.AuthenticationError({ kind: "ExpiredKey" })
error.isRetryable // => falseerror.message // => "ExpiredKey: Your API key has expired. Generate a new one"ContentPolicyError
Section titled “ContentPolicyError”Input or output violated the provider’s content policy. Never retryable without changing the content. The description field is required and explains the violation.
import { AiError } from "effect/unstable/ai"
const error = new AiError.ContentPolicyError({ description: "Input contains prohibited content"})
error.isRetryable // => falseerror.message // => "Content policy violation: Input contains prohibited content"InvalidRequestError
Section titled “InvalidRequestError”The request had malformed or out-of-range parameters. Not retryable — fix the request. Optional parameter, constraint, and description fields compose into the message.
import { AiError } from "effect/unstable/ai"
const error = new AiError.InvalidRequestError({ parameter: "temperature", constraint: "must be between 0 and 2", description: "Temperature value 5 is out of range"})
error.isRetryable // => falseerror.message // => "Invalid request: parameter 'temperature' must be between 0 and 2. Temperature value 5 is out of range"InternalProviderError
Section titled “InternalProviderError”The provider hit an internal error (typically 5xx). Retryable, since these are usually transient. The description field is required.
import { AiError } from "effect/unstable/ai"
const error = new AiError.InternalProviderError({ description: "Server encountered an unexpected error"})
error.isRetryable // => trueerror.message // => "Internal provider error: Server encountered an unexpected error"UnknownError
Section titled “UnknownError”The catch-all for failures that don’t map to any other reason. Not retryable by default. Carries an optional description, provider metadata, and optional http context.
import { AiError } from "effect/unstable/ai"
const error = new AiError.UnknownError({ description: "An unexpected error occurred" })
error.isRetryable // => falseerror.message // => "An unexpected error occurred"Output reasons
Section titled “Output reasons”These describe failures interpreting the model’s output. Because LLM output is non-deterministic, all three carry optional usage info and are retryable.
InvalidOutputError
Section titled “InvalidOutputError”The model’s output could not be parsed or validated. Retryable. Build one from a Schema.SchemaError with InvalidOutputError.fromSchemaError.
import { AiError } from "effect/unstable/ai"
const error = new AiError.InvalidOutputError({ description: "Expected a string but received a number"})
error.isRetryable // => trueerror.message // => "Invalid output: Expected a string but received a number"StructuredOutputError
Section titled “StructuredOutputError”The model produced text that does not conform to the requested structured-output schema (e.g. generateObject). Retryable. Carries the offending responseText, and can be built from a schema error with StructuredOutputError.fromSchemaError(error, responseText).
import { AiError } from "effect/unstable/ai"
const error = new AiError.StructuredOutputError({ description: "Expected a valid JSON object", responseText: '{"foo":}'})
error.isRetryable // => trueerror.responseText // => '{"foo":}'error.message // => "Structured output validation failed: Expected a valid JSON object"UnsupportedSchemaError
Section titled “UnsupportedSchemaError”A codec transformer rejected your output schema because it contains constructs the provider cannot represent (e.g. certain unions). Not retryable — this is a programmer error to fix in code.
import { AiError } from "effect/unstable/ai"
const error = new AiError.UnsupportedSchemaError({ description: "Unions are not supported in Anthropic structured output"})
error.isRetryable // => falseerror.message // => "Unsupported schema: Unions are not supported in Anthropic structured output"Tool-call reasons
Section titled “Tool-call reasons”These describe failures in the tool-calling flow. Tool reasons do not carry http context. Reasons the model can self-correct (bad tool name, bad params) are retryable; bugs in your handlers or configuration are not.
ToolNotFoundError
Section titled “ToolNotFoundError”The model asked to call a tool that isn’t in the toolkit. Retryable, since the model may correct itself given the list of available tools. Carries toolName and availableTools.
import { AiError } from "effect/unstable/ai"
const error = new AiError.ToolNotFoundError({ toolName: "unknownTool", availableTools: ["GetWeather", "GetTime"]})
error.isRetryable // => trueerror.message // => "Tool 'unknownTool' not found. Available tools: GetWeather, GetTime"ToolParameterValidationError
Section titled “ToolParameterValidationError”The model’s tool-call arguments failed schema validation. Retryable — the model may produce valid parameters on a retry. Carries toolName, the raw toolParams JSON, and a description.
import { AiError } from "effect/unstable/ai"
const error = new AiError.ToolParameterValidationError({ toolName: "GetWeather", toolParams: { location: 123 }, description: "Expected string, got number"})
error.isRetryable // => trueerror.message // => "Invalid parameters for tool 'GetWeather': Expected string, got number"InvalidToolResultError
Section titled “InvalidToolResultError”A tool handler returned a value that doesn’t match the tool’s declared result schema. Not retryable — it signals a bug in your handler. Carries toolName and description.
import { AiError } from "effect/unstable/ai"
const error = new AiError.InvalidToolResultError({ toolName: "GetWeather", description: "missing 'temperature' field"})
error.isRetryable // => falseerror.message // => "Tool 'GetWeather' returned invalid result: missing 'temperature' field"ToolResultEncodingError
Section titled “ToolResultEncodingError”A tool result could not be encoded back into JSON to send to the model. Not retryable — fix the tool’s result schema. Carries toolName, the raw toolResult, and a description.
import { AiError } from "effect/unstable/ai"
const error = new AiError.ToolResultEncodingError({ toolName: "GetWeather", toolResult: { temperature: 72n }, description: "Cannot encode bigint values as JSON"})
error.isRetryable // => falseerror.message // => "Failed to encode result for tool 'GetWeather': Cannot encode bigint values as JSON"ToolConfigurationError
Section titled “ToolConfigurationError”A provider-defined tool was configured with invalid arguments. Not retryable — fix the configuration in code. Carries toolName and description.
import { AiError } from "effect/unstable/ai"
const error = new AiError.ToolConfigurationError({ toolName: "OpenAiCodeInterpreter", description: "Invalid container ID format"})
error.isRetryable // => falseerror.message // => "Invalid configuration for tool 'OpenAiCodeInterpreter': Invalid container ID format"ToolkitRequiredError
Section titled “ToolkitRequiredError”An operation needed a toolkit to resolve pending tool-approval responses, but none was provided. Not retryable without supplying a toolkit. Carries pendingApprovals and an optional description.
import { AiError } from "effect/unstable/ai"
const error = new AiError.ToolkitRequiredError({ pendingApprovals: ["GetWeather", "SendEmail"]})
error.isRetryable // => falseerror.message // => "Toolkit required to resolve pending tool approvals: GetWeather, SendEmail"InvalidUserInputError
Section titled “InvalidUserInputError”The prompt contained content that is structurally valid but unsupported by the provider (e.g. an unsupported media type or file format). Not retryable — fix the input.
import { AiError } from "effect/unstable/ai"
const error = new AiError.InvalidUserInputError({ description: "Unsupported media type 'video/mp4'. Supported types include images, application/pdf, text/plain"})
error.isRetryable // => falseerror.message // => "Invalid user input: Unsupported media type 'video/mp4'. ..."Shared schemas
Section titled “Shared schemas”These schemas are attached to reasons to carry extra context. They are plain Effect Schemas, so they decode/encode cleanly across wire boundaries.
ProviderMetadata
Section titled “ProviderMetadata”A Record<string, MutableJson | null> keyed by provider name. Lets a reason carry arbitrary provider-specific detail (error codes, request IDs) without leaking provider types into the public API. Attached as metadata on most provider reasons.
import { AiError } from "effect/unstable/ai"
const error = new AiError.RateLimitError({ metadata: { openai: { errorCode: "rate_limit_exceeded", requestId: "req_123" }, anthropic: null }})
error.metadata.openai // => { errorCode: "rate_limit_exceeded", requestId: "req_123" }UsageInfo
Section titled “UsageInfo”Optional provider-reported token counts: promptTokens, completionTokens, totalTokens. Attached as usage on output reasons (InvalidOutputError, StructuredOutputError) so you can account for tokens even on a failed generation.
import { AiError } from "effect/unstable/ai"
const error = new AiError.InvalidOutputError({ description: "bad output", usage: { promptTokens: 120, completionTokens: 0, totalTokens: 120 }})
error.usage?.totalTokens // => 120HttpContext
Section titled “HttpContext”The captured HTTP exchange behind a provider error: the required request (HttpRequestDetails: method, url, urlParams, hash, headers), an optional response (HttpResponseDetails: status and headers), and an optional raw body string. Attached as http on provider/transport reasons.
import { AiError } from "effect/unstable/ai"
const error = new AiError.InternalProviderError({ description: "Server error", http: { request: { method: "POST", url: "https://api.openai.com/v1/chat/completions", urlParams: [], hash: undefined, headers: { "Content-Type": "application/json" } }, response: { status: 503, headers: { "X-Request-Id": "req_abc123" } }, body: '{"error":"service unavailable"}' }})
error.http?.response?.status // => 503See also
Section titled “See also”- Language Model — the operations (
generateText,generateObject, streaming) that fail withAiError. - Tools — defining toolkits; tool failures surface as the tool reasons above.