# 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:

- `module` and `method` — 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**](https://effect.plants.sh/error-management/reason-errors/): 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`](https://effect.plants.sh/error-management/reason-errors/) helper to narrow straight to a reason.

```ts
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}`)
  )
)
```
**Unstable module:** The AI modules live under `effect/unstable/ai`. Import the `AiError` namespace from
`effect/unstable/ai`. The error class string identifiers are namespaced as
`effect/ai/AiError/...` but you never need to write those by hand.

## The common case: catch, branch, inspect

### Catching by tag, branching on the reason

Because the error channel only ever contains `AiError`, [`Effect.catchTag`](https://effect.plants.sh/error-management/catching-errors/) with `"AiError"` recovers from *all* AI failures in one place. Switch on `reason._tag` to decide what to do:

```ts
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`

When you only care about one reason, [`Effect.catchReason`](https://effect.plants.sh/error-management/reason-errors/) 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:

```ts
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`

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):

```ts
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

Provider-facing reasons attach extra forensic context. Reach into `reason` to pull it out:

- `reason.http` — an `HttpContext` (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 like `InvalidOutputError`.

```ts
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)
  })
)
```
**Why the reason pattern?:** Keeping a single `AiError` in the error channel means a swappable provider never
changes your function signatures. The semantic detail lives in `reason`, which is
fully discriminated by `_tag`, so you get exhaustive `switch` checking without
exposing provider internals.

## Reference

All of the following are exported from `effect/unstable/ai` via the `AiError` namespace.

### `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>"`.

```ts
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"
```

### `make`

Constructor for an `AiError` from a `module`, `method`, and `reason`. Equivalent to `new AiError.AiError({ ... })`; prefer it when constructing errors inside provider integrations.

```ts
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`

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.

```ts
import { Schema } from "effect"
import { AiError } from "effect/unstable/ai"

// As a type
const handle = (reason: AiError.AiErrorReason) => reason._tag

// As a schema (decode an unknown reason)
const decode = Schema.decodeUnknownSync(AiError.AiErrorReason)
```

### `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.

```ts
import { AiError } from "effect/unstable/ai"

type Encoded = AiError.AiErrorEncoded
// => { _tag: "AiError"; module: string; method: string; reason: ... }
```

### `isAiError`

Type guard narrowing an `unknown` value to `AiError`. Handy at boundaries where the error type has been widened to `unknown`.

```ts
import { AiError } from "effect/unstable/ai"

const aiError = AiError.make({
  module: "Test",
  method: "x",
  reason: new AiError.RateLimitError({})
})

AiError.isAiError(aiError)            // => true
AiError.isAiError(new Error("oops"))  // => false
```

### `isAiErrorReason`

Type guard narrowing an `unknown` value to one of the `AiErrorReason` classes (i.e. a value carrying the reason brand).

```ts
import { AiError } from "effect/unstable/ai"

AiError.isAiErrorReason(new AiError.RateLimitError({})) // => true
AiError.isAiErrorReason(new Error("oops"))              // => false
```

### `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`).

```ts
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

These reasons describe failures coming from (or before reaching) the provider. Most carry optional `http` context and provider `metadata`.

### `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`.

```ts
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`

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.

```ts
import { Duration } from "effect"
import { AiError } from "effect/unstable/ai"

const error = new AiError.RateLimitError({ retryAfter: Duration.seconds(60) })

error.isRetryable // => true
error.retryAfter  // => Duration("60 seconds")
error.message     // => "Rate limit exceeded. Retry after 1m"
```

### `QuotaExhaustedError`

Account or billing limits have been reached. Not retryable without user action; an optional `resetAt` `DateTimeUtc` indicates when the quota refreshes.

```ts
import { AiError } from "effect/unstable/ai"

const error = new AiError.QuotaExhaustedError({})

error.isRetryable // => false
error.message     // => "Quota exhausted. Check your account billing and usage limits."
```

### `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`.

```ts
import { AiError } from "effect/unstable/ai"

const error = new AiError.AuthenticationError({ kind: "ExpiredKey" })

error.isRetryable // => false
error.message     // => "ExpiredKey: Your API key has expired. Generate a new one"
```

### `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.

```ts
import { AiError } from "effect/unstable/ai"

const error = new AiError.ContentPolicyError({
  description: "Input contains prohibited content"
})

error.isRetryable // => false
error.message     // => "Content policy violation: Input contains prohibited content"
```

### `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.

```ts
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 // => false
error.message     // => "Invalid request: parameter 'temperature' must be between 0 and 2. Temperature value 5 is out of range"
```

### `InternalProviderError`

The provider hit an internal error (typically 5xx). Retryable, since these are usually transient. The `description` field is required.

```ts
import { AiError } from "effect/unstable/ai"

const error = new AiError.InternalProviderError({
  description: "Server encountered an unexpected error"
})

error.isRetryable // => true
error.message     // => "Internal provider error: Server encountered an unexpected error"
```

### `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.

```ts
import { AiError } from "effect/unstable/ai"

const error = new AiError.UnknownError({ description: "An unexpected error occurred" })

error.isRetryable // => false
error.message     // => "An unexpected error occurred"
```

## 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`

The model's output could not be parsed or validated. Retryable. Build one from a `Schema.SchemaError` with `InvalidOutputError.fromSchemaError`.

```ts
import { AiError } from "effect/unstable/ai"

const error = new AiError.InvalidOutputError({
  description: "Expected a string but received a number"
})

error.isRetryable // => true
error.message     // => "Invalid output: Expected a string but received a number"
```

### `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)`.

```ts
import { AiError } from "effect/unstable/ai"

const error = new AiError.StructuredOutputError({
  description: "Expected a valid JSON object",
  responseText: '{"foo":}'
})

error.isRetryable  // => true
error.responseText // => '{"foo":}'
error.message      // => "Structured output validation failed: Expected a valid JSON object"
```

### `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.

```ts
import { AiError } from "effect/unstable/ai"

const error = new AiError.UnsupportedSchemaError({
  description: "Unions are not supported in Anthropic structured output"
})

error.isRetryable // => false
error.message     // => "Unsupported schema: Unions are not supported in Anthropic structured output"
```

## Tool-call reasons

These describe failures in the [tool-calling](https://effect.plants.sh/ai/tools/) 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`

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`.

```ts
import { AiError } from "effect/unstable/ai"

const error = new AiError.ToolNotFoundError({
  toolName: "unknownTool",
  availableTools: ["GetWeather", "GetTime"]
})

error.isRetryable // => true
error.message     // => "Tool 'unknownTool' not found. Available tools: GetWeather, GetTime"
```

### `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`.

```ts
import { AiError } from "effect/unstable/ai"

const error = new AiError.ToolParameterValidationError({
  toolName: "GetWeather",
  toolParams: { location: 123 },
  description: "Expected string, got number"
})

error.isRetryable // => true
error.message     // => "Invalid parameters for tool 'GetWeather': Expected string, got number"
```

### `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`.

```ts
import { AiError } from "effect/unstable/ai"

const error = new AiError.InvalidToolResultError({
  toolName: "GetWeather",
  description: "missing 'temperature' field"
})

error.isRetryable // => false
error.message     // => "Tool 'GetWeather' returned invalid result: missing 'temperature' field"
```

### `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`.

```ts
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 // => false
error.message     // => "Failed to encode result for tool 'GetWeather': Cannot encode bigint values as JSON"
```

### `ToolConfigurationError`

A provider-defined tool was configured with invalid arguments. Not retryable — fix the configuration in code. Carries `toolName` and `description`.

```ts
import { AiError } from "effect/unstable/ai"

const error = new AiError.ToolConfigurationError({
  toolName: "OpenAiCodeInterpreter",
  description: "Invalid container ID format"
})

error.isRetryable // => false
error.message     // => "Invalid configuration for tool 'OpenAiCodeInterpreter': Invalid container ID format"
```

### `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`.

```ts
import { AiError } from "effect/unstable/ai"

const error = new AiError.ToolkitRequiredError({
  pendingApprovals: ["GetWeather", "SendEmail"]
})

error.isRetryable // => false
error.message     // => "Toolkit required to resolve pending tool approvals: GetWeather, SendEmail"
```

### `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.

```ts
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 // => false
error.message     // => "Invalid user input: Unsupported media type 'video/mp4'. ..."
```

## Shared schemas

These schemas are attached to reasons to carry extra context. They are plain Effect `Schema`s, so they decode/encode cleanly across wire boundaries.

### `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.

```ts
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`

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.

```ts
import { AiError } from "effect/unstable/ai"

const error = new AiError.InvalidOutputError({
  description: "bad output",
  usage: { promptTokens: 120, completionTokens: 0, totalTokens: 120 }
})

error.usage?.totalTokens // => 120
```

### `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.

```ts
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 // => 503
```
**Header redaction:** Header values in `HttpContext` are typed as `string | Redacted<string>`. Sensitive
headers (such as `Authorization`) are redacted automatically when an error is built
from a platform request, so logging the context does not leak credentials.

## See also

- [Language Model](https://effect.plants.sh/ai/language-model/) — the operations (`generateText`, `generateObject`, streaming) that fail with `AiError`.
- [Tools](https://effect.plants.sh/ai/tools/) — defining toolkits; tool failures surface as the tool reasons above.