Enact.Error (Enact v0.1.0)

Copy Markdown View Source

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.

Summary

Functions

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

Builds a :forbidden error with an optional reason.

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

Builds an :invalid error carrying the changeset.

Builds a :not_found error with an optional reason.

Types

t()

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

type()

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

Functions

conflict(reason \\ nil, meta \\ %{})

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

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

forbidden(reason \\ nil)

@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(reason)

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

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

invalid(changeset)

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

Builds an :invalid error carrying the changeset.

not_found(reason \\ nil)

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

Builds a :not_found error with an optional reason.