# `Enact.Error`
[🔗](https://github.com/svycal/enact/blob/v0.1.0/lib/enact/error.ex#L1)

The closed error taxonomy for action results.

Every failed `Enact.run/3`, `Enact.dry_run/3`, `Enact.subject/3`, or
`Enact.authorized/3` returns
`{:error, %Enact.Error{}}` with one of five types, each mapping to an
HTTP status:

  * `:invalid` → 422 — bad input; carries the changeset (the only
    outward-rich type; it describes the caller's own input)
  * `:forbidden` → 403 — the actor may not perform this action
  * `:not_found` → 404 — the URL-anchored subject does not exist
    (or is outside the caller's tenant)
  * `:conflict` → 409 — input was fine; the world changed
    (optimistic/temporal races, confirmation-digest mismatch)
  * `:internal` → 500 — a bug bucket; every production occurrence is
    either a bug or a missing promotion to a real type

`reason` is a stable term for logs, telemetry, and the host renderer.
Do not echo it wholesale in responses. The default renderer maps `type`
to generic copy. To vary the message — usually on `:forbidden` — match
a host-owned reason in the renderer and keep the generic clause as
fallback. Store atoms, not user-facing strings. Do not use `:not_found`
reasons to distinguish "exists" from "not yours".

`meta` is for machine-readable extras (`retry_after`, stable error codes).

Actions never invent bespoke error *types* — the closed taxonomy means
the host app's renderer never grows a default clause for `type`.

# `t`

```elixir
@type t() :: %Enact.Error{
  changeset: Ecto.Changeset.t() | nil,
  meta: map(),
  reason: term(),
  type: type()
}
```

# `type`

```elixir
@type type() :: :invalid | :forbidden | :not_found | :conflict | :internal
```

# `conflict`

```elixir
@spec conflict(term(), map()) :: t()
```

Builds a `:conflict` error with an optional reason and meta.

# `forbidden`

```elixir
@spec forbidden(term()) :: t()
```

Builds a `:forbidden` error with an optional reason.

The runner produces this from `authorize/1`: `false` yields no reason;
`{:error, reason}` stores that reason. Use a host-owned atom when the
renderer should vary the 403 copy. Do not echo `reason` wholesale.

# `internal`

```elixir
@spec internal(term()) :: t()
```

Builds an `:internal` error with a reason for logs and telemetry.

# `invalid`

```elixir
@spec invalid(Ecto.Changeset.t()) :: t()
```

Builds an `:invalid` error carrying the changeset.

# `not_found`

```elixir
@spec not_found(term()) :: t()
```

Builds a `:not_found` error with an optional reason.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
