# Issues & Error Formatting

When decoding or encoding fails, Schema does not throw away the details. It
produces a `SchemaIssue.Issue` — a recursive tree that records *what* went wrong
and *where* in the input. You can render it as a human-readable string, walk it
to build custom output, or convert it to a Standard Schema failure result.

```ts
import { Schema, Result } from "effect"

const Person = Schema.Struct({
  name: Schema.String,
  age: Schema.Number
})

// `decodeUnknownResult` returns Result<Person, SchemaIssue.Issue> — the failure
// channel carries the issue tree directly (not a SchemaError wrapper), as data
// rather than an exception. `{ errors: "all" }` collects every problem at once.
const result = Schema.decodeUnknownResult(Person, { errors: "all" })({
  name: 42
})

if (Result.isFailure(result)) {
  // result.failure IS the SchemaIssue.Issue tree; String(...) renders it.
  console.error(String(result.failure))
}
```

## Where the issue lives

How you reach the issue depends on the runner you chose in
[basic usage](https://effect.plants.sh/schema/basic-usage/):

- **`decodeUnknownResult`** — the failure channel carries the
  `SchemaIssue.Issue` tree *directly* (no wrapper); `result.failure` is the issue.
- **`decodeUnknownEffect`** — the Effect fails with a `Schema.SchemaError` in its
  error channel; read `.issue` for the tree.
- **`decodeUnknownExit`** — the `Exit` fails with a `Schema.SchemaError`
  (`Exit.Exit<A, Schema.SchemaError>`); read `.issue` for the tree.
- **`decodeUnknownSync`** — throws a plain `Error` whose `message` is the
  formatted string (`issue.toString()`) and whose `cause` holds the
  `SchemaIssue.Issue` tree.
- **`decodeUnknownOption`** — collapses the failure to `Option.none()`; the issue
  is discarded.
**Note:** The `Effect` and `Exit` runners wrap the issue in a `Schema.SchemaError`. The
`Result` runner puts the raw `SchemaIssue.Issue` in the failure channel, and the
sync runner throws a plain `Error` carrying the issue in its `cause`.

```ts
import { Schema, SchemaIssue } from "effect"

const decode = Schema.decodeUnknownSync(Schema.Number)

try {
  decode("not a number")
} catch (err) {
  if (err instanceof Error) {
    // err.message is the formatted string; err.cause is the structured tree.
    console.error(err.message)
    // Expected number, got "not a number"
    console.log(SchemaIssue.isIssue(err.cause)) // => true
  }
}
```

The `Result` runner does not wrap anything — `result.failure` is the
`SchemaIssue.Issue` itself, so its `_tag` is the issue tag (e.g. `InvalidType`),
and `SchemaIssue.isIssue` returns `true` for it.

```ts
import { Schema, SchemaIssue } from "effect"

const result = Schema.decodeUnknownResult(Schema.Number)("oops")
if (result._tag === "Failure") {
  const issue = result.failure
  console.log(issue._tag) // => "InvalidType"
  console.log(SchemaIssue.isIssue(issue)) // => true
}
```
**Note:** Use `Schema.isSchemaError` when handling the *Effect* or *Exit* runner's failure
channel — those are the runners that wrap the issue in a `Schema.SchemaError`.
`SchemaError` is a plain class (not an `Error` subclass) carrying `_tag:
"SchemaError"`, an `issue` field, and a lazy `message` getter
(`issue.toString()`).

## errors: "first" vs "all"

The `errors` field of `ParseOptions` decides how much of the tree gets built. The
default `"first"` aborts at the first problem; `"all"` keeps validating siblings
and aggregates every failure under a `Composite` node.

```ts
import { Schema } from "effect"

const Person = Schema.Struct({
  name: Schema.String,
  age: Schema.Number
})

// "first" (default): stops at the first failing field.
const first = Schema.decodeUnknownResult(Person)({ name: 1, age: "x" })
// => Failure with a single MissingKey/InvalidType issue

// "all": collects every failing field into one Composite tree.
const all = Schema.decodeUnknownResult(Person, { errors: "all" })({
  name: 1,
  age: "x"
})
// => Failure aggregating both field errors
```

## The default string formatter

Calling `String(issue)` (or `issue.toString()`) uses the default formatter. It
renders each leaf failure with its message and the property path at which it
occurred.

```ts
import { Schema, Effect } from "effect"

const Person = Schema.Struct({
  name: Schema.String,
  age: Schema.Number
})

const program = Schema.decodeUnknownEffect(Person, { errors: "all" })({}).pipe(
  // The error channel holds a SchemaError; format its issue for logging.
  Effect.catch((err) => Effect.logError(String(err.issue)))
)
```

When the input is missing both keys with `{ errors: "all" }`, the formatted
output names each failing path:

```
Missing key
  at ["name"]
Missing key
  at ["age"]
```
**Tip:** Annotate a schema with a `title` or `identifier` to make messages friendlier —
`Schema.Struct({ ... }).annotate({ title: "Person" })` uses `"Person"` in the
output instead of the structural type description.

## Inspecting the issue tree

For programmatic handling — mapping failures to your own error type, grouping by
field, building a localized message — pattern-match on the issue's `_tag`. The
tree is a discriminated union; the terminal *leaf* nodes are the ones you usually
care about.

```ts
import { SchemaIssue } from "effect"

// Describe an issue in your own words by matching on its tag.
function describe(issue: SchemaIssue.Issue): string {
  switch (issue._tag) {
    case "MissingKey":
      return "a required field is missing"
    case "InvalidType":
      return "a field has the wrong type"
    case "InvalidValue":
      return "a field failed a constraint"
    default:
      // Composite nodes (Pointer, Composite, Filter, ...) wrap inner issues.
      return String(issue)
  }
}

void describe // illustrative
```

The composite nodes — `Pointer`, `Composite`, `Filter`, `Encoding`, `AnyOf` —
wrap inner issues to add context such as a path segment or the filter that
failed. `SchemaIssue.getActual(issue)` extracts the offending input value where
one is available.

## Standard Schema output

To integrate with tooling that speaks the
[Standard Schema](https://github.com/standard-schema/standard-schema) spec — form
libraries, validators — build a formatter that produces a `FailureResult` of
`{ message, path }` entries instead of a string.

```ts
import { Schema, SchemaIssue, Result } from "effect"

const Login = Schema.Struct({
  username: Schema.String.check(Schema.isNonEmpty()),
  password: Schema.String.check(Schema.isMinLength(8))
})

const formatter = SchemaIssue.makeFormatterStandardSchemaV1()

const result = Schema.decodeUnknownResult(Login, { errors: "all" })({
  username: "",
  password: "short"
})

if (Result.isFailure(result)) {
  // result.failure is the SchemaIssue.Issue; format it directly.
  // { issues: [{ message, path }, ...] } — one entry per failing field.
  console.error(formatter(result.failure).issues)
}
```

Or skip the manual decode and hand the schema directly to a Standard Schema
consumer with [`Schema.toStandardSchemaV1`](#schematostandardschemav1).

---

## The Issue ADT

`SchemaIssue.Issue` is a `_tag`-discriminated union. **Leaf** nodes are terminal;
**composite** nodes wrap one or more inner issues to add context. Every node has
a `toString()` that delegates to the default formatter.

| Tag             | Kind      | Produced when |
| --------------- | --------- | ------------- |
| `InvalidType`   | leaf      | runtime type mismatch (e.g. `string` expected, got `null`) |
| `InvalidValue`  | leaf      | right type, wrong value (failed value constraint) |
| `MissingKey`    | leaf      | a required struct key / tuple index is absent |
| `UnexpectedKey` | leaf      | an extra key is present under strict validation |
| `Forbidden`     | leaf      | a forbidden operation (e.g. async work in a sync runner) |
| `OneOf`         | leaf      | a `oneOf` union matched more than one member |
| `Filter`        | composite | a refinement check (`Schema.makeFilter` / `.check`) rejected the value |
| `Encoding`      | composite | a transformation (`decodeTo` / `encodeTo`) failed |
| `Pointer`       | composite | adds a property-key path to an inner issue |
| `Composite`     | composite | aggregates sibling issues under one schema node |
| `AnyOf`         | composite | a value matched *no* member of a union |

### InvalidType

The runtime type of the input does not match the schema. `actual` is
`Option<unknown>` (`None` when no value was provided). Default message:
`Expected <type>, got <actual>`.

```ts
import { Schema } from "effect"

const r = Schema.decodeUnknownResult(Schema.String)(42)
if (r._tag === "Failure") {
  console.log(r.failure._tag) // => "InvalidType"
  console.log(String(r.failure)) // => "Expected string, got 42"
}
```

### InvalidValue

The input has the correct type but violates a value constraint. `actual` is
`Option<unknown>`; an optional `annotations.message` overrides the default
`Invalid data <actual>`.

```ts
import { Option, SchemaIssue } from "effect"

const issue = new SchemaIssue.InvalidValue(Option.some(""), {
  message: "must not be empty"
})
console.log(String(issue)) // => "must not be empty"
```

### MissingKey

A required struct key or tuple index is absent. Carries no `actual` value;
`annotations` may hold a custom `messageMissingKey`. Default message: `Missing
key`.

```ts
import { SchemaIssue } from "effect"

const issue = new SchemaIssue.MissingKey(undefined)
console.log(String(issue)) // => "Missing key"
```

### UnexpectedKey

An object/tuple contains a key the schema did not declare (strict validation).
`actual` is the raw value at that key; `ast` is the schema. Default message:
`Unexpected key with value <actual>`.

```ts
import { SchemaIssue } from "effect"

// `ast` comes from a real schema node; matched here by `_tag`.
function isExtra(issue: SchemaIssue.Issue): boolean {
  return issue._tag === "UnexpectedKey"
}
void isExtra
```

### Forbidden

A forbidden operation was hit during parsing — most commonly an asynchronous
Effect running inside a synchronous runner like `decodeUnknownSync`. Default
message: `Forbidden operation`.

```ts
import { Option, SchemaIssue } from "effect"

const issue = new SchemaIssue.Forbidden(Option.none(), {
  message: "async not allowed in sync context"
})
console.log(String(issue)) // => "async not allowed in sync context"
```

### OneOf

A value matched *more than one* member of a union configured for exactly-one
matching. `successes` lists the AST nodes that accepted it. Default message:
`Expected exactly one member to match the input <actual>`.

```ts
import { SchemaIssue } from "effect"

function isAmbiguous(issue: SchemaIssue.Issue): boolean {
  return issue._tag === "OneOf"
}
void isAmbiguous
```

### Filter

A refinement check (`Schema.makeFilter`, `.check(...)`) rejected the value. `actual`
is the tested value, `filter` is the AST check node, and `issue` is the inner
failure reason.

```ts
import { SchemaIssue } from "effect"

function describeFilter(issue: SchemaIssue.Issue): string {
  if (issue._tag === "Filter") {
    return `Filter failed on: ${JSON.stringify(issue.actual)}`
  }
  return String(issue)
}
void describeFilter
```

See [filters](https://effect.plants.sh/schema/filters/) for the checks that produce `Filter` issues.

### Encoding

A transformation step (`Schema.decodeTo` / `Schema.encodeTo`) failed. `ast` is
the transformation node, `actual` is `Option<unknown>`, and `issue` is the inner
failure. Formatters recurse straight into `issue`.

```ts
import { Schema, SchemaIssue } from "effect"

// NumberFromString decodes "abc" -> fails inside the transform.
const r = Schema.decodeUnknownResult(Schema.NumberFromString)("abc")
if (r._tag === "Failure") {
  console.log(SchemaIssue.isIssue(r.failure)) // => true
}
```

### Pointer

Wraps an inner issue with a `path` (array of property keys) marking *where* the
error occurred. Carries no `actual`. Formatters concatenate nested pointer paths
into one path like `["a"]["b"][0]`.

```ts
import { SchemaIssue } from "effect"

const inner = new SchemaIssue.MissingKey(undefined)
const issue = new SchemaIssue.Pointer(["age"], inner)
console.log(String(issue))
// => "Missing key\n  at [\"age\"]"
```

### Composite

Groups multiple child `issues` (non-empty) under one schema `ast` node — this is
what `errors: "all"` builds for structs and tuples. `actual` is
`Option<unknown>`. Formatters flatten it by recursing into each child.

```ts
import { Schema } from "effect"

const Point = Schema.Struct({ x: Schema.Number, y: Schema.Number })
const r = Schema.decodeUnknownResult(Point, { errors: "all" })({
  x: "a",
  y: "b"
})
if (r._tag === "Failure") {
  console.log(r.failure._tag) // => "Composite"
}
```

### AnyOf

A value matched *no* member of a union. `issues` holds per-member failures; when
empty, the formatter falls back to the union's `expected` annotation.

```ts
import { Schema } from "effect"

const U = Schema.Union([Schema.String, Schema.Number])
const r = Schema.decodeUnknownResult(U)(true)
if (r._tag === "Failure") {
  console.log(r.failure._tag) // => "AnyOf"
}
```

## Issue helpers

### isIssue

Type guard narrowing `unknown` to `Issue`, useful in catch-all handlers.

```ts
import { SchemaIssue } from "effect"

const issue = new SchemaIssue.MissingKey(undefined)
console.log(SchemaIssue.isIssue(issue)) // => true
console.log(SchemaIssue.isIssue("nope")) // => false
```

### getActual

Returns the offending input as `Option<unknown>`, normalizing across variants.
Returns `None` for `Pointer` and `MissingKey` (they carry no value), the stored
`Option` for `InvalidType` / `InvalidValue` / `Forbidden` / `Encoding` /
`Composite`, and `Option.some(actual)` for `AnyOf` / `UnexpectedKey` / `OneOf` /
`Filter`.

```ts
import { Option, SchemaIssue } from "effect"

const a = SchemaIssue.getActual(new SchemaIssue.MissingKey(undefined))
console.log(a) // => { _tag: "None" }

const b = SchemaIssue.getActual(
  new SchemaIssue.InvalidValue(Option.some(""))
)
console.log(b) // => { _tag: "Some", value: "" }
```

## Formatters

A `SchemaIssue.Formatter<Format>` is a function `Issue => Format`. The module
ships two factories plus the customizable hooks they build on.

### makeFormatterDefault

Creates a `Formatter<string>` that flattens the tree into `{ message, path }`
entries and renders each as `<message>` or `<message>\n  at <path>`, joined by
newlines. This is the formatter behind `Issue.toString()`.

```ts
import { Schema, SchemaIssue } from "effect"

const format = SchemaIssue.makeFormatterDefault()
const r = Schema.decodeUnknownResult(Schema.String)(1)
if (r._tag === "Failure") {
  console.log(format(r.failure)) // => "Expected string, got 1"
}
```

### makeFormatterStandardSchemaV1

Creates a `Formatter<StandardSchemaV1.FailureResult>` producing `{ issues:
[{ message, path }] }`. `Pointer` paths are accumulated into full property paths.
Optionally accepts `leafHook` / `checkHook` to customize messages.

```ts
import { SchemaIssue } from "effect"

const format = SchemaIssue.makeFormatterStandardSchemaV1()
const issue = new SchemaIssue.Pointer(
  ["name"],
  new SchemaIssue.MissingKey(undefined)
)
console.log(format(issue))
// => { issues: [{ message: "Missing key", path: ["name"] }] }
```

### defaultFormatter

The shared `Formatter<string>` instance (the result of `makeFormatterDefault()`)
that `Issue.toString()` delegates to.

```ts
import { Option, SchemaIssue } from "effect"

const issue = new SchemaIssue.InvalidValue(Option.some(3))
console.log(SchemaIssue.defaultFormatter(issue)) // => "Invalid data 3"
```

### defaultLeafHook

The built-in `LeafHook`: maps a terminal issue to a string, honoring a `message`
annotation when present, otherwise the per-`_tag` default (`Expected <type>, got
<actual>`, `Invalid data <actual>`, `Missing key`, etc.). Reuse it as a base for
custom hooks.

```ts
import { Option, SchemaIssue } from "effect"

const msg = SchemaIssue.defaultLeafHook(
  new SchemaIssue.InvalidValue(Option.some(""))
)
console.log(msg) // => "Invalid data \"\""
```

### defaultCheckHook

The built-in `CheckHook` for `Filter` issues: returns the inner issue's `message`
annotation, then the filter's, or `undefined` to let the formatter fall back to
`Expected <filter>, got <actual>`.

```ts
import { SchemaIssue } from "effect"

// Pass the default hooks explicitly when overriding only one of them.
const format = SchemaIssue.makeFormatterStandardSchemaV1({
  leafHook: SchemaIssue.defaultLeafHook,
  checkHook: SchemaIssue.defaultCheckHook
})
void format
```

### Customizing messages with hooks

Both formatter factories accept `leafHook` (a `LeafHook`, `(issue: Leaf) =>
string`) and `checkHook` (a `CheckHook`, `(issue: Filter) => string |
undefined`). Provide your own to localize or rebrand messages.

```ts
import { SchemaIssue } from "effect"

const format = SchemaIssue.makeFormatterStandardSchemaV1({
  leafHook: (issue) =>
    issue._tag === "MissingKey" ? "Champ requis" : SchemaIssue.defaultLeafHook(issue)
})

const issue = new SchemaIssue.Pointer(
  ["email"],
  new SchemaIssue.MissingKey(undefined)
)
console.log(format(issue).issues)
// => [{ message: "Champ requis", path: ["email"] }]
```

### redact

Walks an issue and replaces every captured `actual` value with a `Redacted`
wrapper, so logging the tree never leaks sensitive input (passwords, tokens).
`MissingKey` is returned unchanged (it has no value); other variants collapse to
a redacted `InvalidValue` or keep structure with redacted values.
**Caution:** `SchemaIssue.redact` is exported but marked `@internal`; its signature may change
across minor releases. For stable redaction of secret fields prefer wrapping the
field schema in `Schema.Redacted` so the value never reaches the issue in clear
text in the first place.

```ts
import { Option, SchemaIssue } from "effect"

const issue = new SchemaIssue.InvalidValue(Option.some("hunter2"))
const safe = SchemaIssue.redact(issue)
console.log(safe._tag) // => "InvalidValue"
// safe.actual now holds a Redacted value that prints as <redacted>
```

## Schema-level entry points

### Schema.SchemaError / Schema.isSchemaError

`SchemaError` is the error type the **Effect** and **Exit** runners place in
their failure channel (`decodeUnknownEffect` / `decodeUnknownExit` and their
encode counterparts). It is a plain class with `_tag: "SchemaError"`, an `issue`
field, and a `message` getter (`issue.toString()`). `isSchemaError` narrows an
`unknown` to it.

```ts
import { Schema, Effect } from "effect"

const program = Schema.decodeUnknownEffect(Schema.Number)("oops").pipe(
  Effect.catch((err) => {
    // err is a SchemaError here, narrowed by the error channel type.
    console.log(err._tag) // => "SchemaError"
    console.log(err.message) // => "Expected number, got \"oops\""
    // err.issue is the structured SchemaIssue.Issue tree
    return Effect.void
  })
)

void program
```

Use `Schema.isSchemaError` when narrowing an `unknown` (for instance, a value
caught from arbitrary code) to a `SchemaError`.

### Schema.toStandardSchemaV1

Wraps a schema as a [Standard Schema v1](https://standardschema.dev/) object. Its
`~standard.validate` decodes synchronously (returning a `Promise` only if the
schema has async parts) and reports failures via the Standard Schema formatter.
Defaults to `errors: "all"`; accepts `leafHook`, `checkHook`, and `parseOptions`.

```ts
import { Schema } from "effect"

const Person = Schema.Struct({
  name: Schema.NonEmptyString,
  age: Schema.Number.check(Schema.isBetween({ minimum: 0, maximum: 150 }))
})

const standard = Schema.toStandardSchemaV1(Person)

console.log(standard["~standard"].validate({ name: "Alice", age: 30 }))
// => { value: { name: "Alice", age: 30 } }

console.log(standard["~standard"].validate({ name: "", age: 200 }))
// => { issues: [{ path: ["name"], message: ... }, { path: ["age"], message: ... }] }
```

### Schema.toStandardJSONSchemaV1

Attaches an experimental Standard JSON Schema v1 `jsonSchema` provider to the
schema, exposing `input` / `output` JSON Schemas for `draft-2020-12` or
`draft-07` targets. See [JSON Schema](https://effect.plants.sh/schema/json-schema/) for the underlying
generation.

```ts
import { Schema } from "effect"

const Person = Schema.Struct({ name: Schema.String, age: Schema.Number })
const withJson = Schema.toStandardJSONSchemaV1(Person)

const input = withJson["~standard"].jsonSchema.input({ target: "draft-07" })
console.log(input.type) // => "object"
```

---

Next, see how to define your own typed errors with
[`Schema.TaggedErrorClass`](https://effect.plants.sh/schema/defining-errors/), or revisit the
[runners that surface these issues](https://effect.plants.sh/schema/basic-usage/).