View Source Getting started
This guide takes you from an empty mix.exs to your first named error type —
returned as a value, raised as an exception, and handled at a boundary — in a
few minutes. It assumes no prior knowledge of Errata.
For the complete API reference, see the Errata module.
Installation
Add errata to your dependencies in mix.exs:
def deps do
[
{:errata, "~> 1.9"}
]
endThen run mix deps.get. Errata's only required dependency is :telemetry, and
it starts no processes — there is nothing to add to your supervision tree.
JSON encoding needs no configuration. On Elixir 1.18 and later every error type
implements the built-in JSON.Encoder; if jason is in your dependencies it
implements Jason.Encoder as well. Both emit the same shape.
The big idea
Elixir gives you two ways to signal failure, and most applications use both. A
function returns {:error, reason}, or it raises. Errata does not ask you to
pick one — it asks what the failure is.
The usual alternatives lose that. {:error, :not_found} is an atom that could
have come from anywhere and carries nothing but itself; by the time it reaches a
boundary, the order id, the module, and the line that produced it are gone. A
bare raise "order not found" is worse — now it is a string.
An Errata error is a named type. It is an ordinary Exception struct, so it can
be returned in a tuple or raised, and it carries its context with it either
way:
message— a human-readable descriptionreason— an atom that classifies it, for pattern matchingcontext— a map of whatever metadata matters at the point of failurecause— the lower-level error this one wrapped, if anyenv— the module, function, file, line, and stacktrace where it was created
Taken together, your application's error types become a catalogue of the ways it can fail — an errata sheet for the system.
Define your first error type
Pick a kind. Errata has three, and the choice is about how a boundary should treat the error, not about how your domain logic branches on it:
defmodule MyApp.Orders.OrderNotFound do
use Errata.DomainError,
default_message: "the requested order does not exist"
endErrata.DomainError is for business-rule violations and other failures inside
the problem domain. Errata.InfrastructureError is for timeouts, database
failures, and the like. Errata.Error is the base, for anything that fits
neither. Prefer the first two: they make the classification explicit, and the
Errata guards can then act on it anywhere in the system.
That one line generates the exception struct, the Errata.Error behaviour, and
the String.Chars and JSON protocol implementations.
Define error types in
lib/This is the trap that costs the most time, because it fails far from its cause. Protocol implementations are consolidated when your project compiles, so an error type defined after that point — in a
.exsscript, aniexsession, or inside a test module body — gets none of them. You get three "protocol has already been consolidated" warnings at compile time, and then, much later and somewhere else entirely, aProtocol.UndefinedErrorfrom something as innocent asto_string/1.Define error types in
lib/. In tests, define fixture types at the top level of the test file, above the test module, or setconsolidate_protocols: Mix.env() != :testinmix.exs. See Testing with Errata.
Return it as a value
Add use Errata to the module that creates errors. It requires Errata (the
creation macros need it) and imports the three guards at the same time:
defmodule MyApp.Orders do
use Errata
alias MyApp.Orders.OrderNotFound
def fetch_order(id) do
with :error <- lookup(id) do
{:error, Errata.create(OrderNotFound, reason: :not_found, context: %{order_id: id})}
end
end
endErrata.create/2 is the one to reach for by default. It is a macro, which is
what lets it capture __ENV__ and the stacktrace into the error's :env field,
and because it takes the type as an argument, a single use Errata covers every
error type the module creates.
Two variants exist for when that does not fit. OrderNotFound.create/1 does the
same thing and reads better when a module works mostly with one type, but being
a macro on the error module, that module must be required. OrderNotFound.new/1
is a plain function that skips the :env capture — use it where a macro cannot
go, such as apply/3 or a captured &OrderNotFound.new/1, and in test fixtures
where env: nil keeps structs easy to compare.
Or raise it
The same type, unchanged:
raise MyApp.Orders.OrderNotFound, reason: :not_found, context: %{order_id: 42}This is the point of the design. You do not define one type for the value path and another for the exception path — you define the error once and decide at each call site how to signal it.
Handle it
use Errata brought three guards into scope, so you can branch on whether
something is an Errata error at all, and on its kind:
def handle({:ok, order}), do: order
def handle({:error, e}) when is_domain_error(e), do: render_user_message(e)
def handle({:error, e}) when is_infrastructure_error(e), do: retry_later(e)
def handle({:error, e}) when is_error(e), do: report(e)
def handle({:error, other}), do: report_unknown(other)Guard first. The accessors raise on a value that is not an Errata error, so a
pipeline that assumes every {:error, _} holds one will fail on the first
foreign error it meets. Errata.to_error/2 is the other way round: it converts
whatever it is handed into an Errata error, letting you normalise once and treat
everything uniformly after that.
Get it out of the system
At a boundary, an error becomes a log line, a telemetry event, an HTTP response, or JSON — and it still has everything it started with:
to_string(error)
#=> "the requested order does not exist: :not_found"
Errata.to_map(error)
#=> %{error_type: "MyApp.Orders.OrderNotFound", reason: :not_found, ...}
Errata.log(error)
Errata.report(error)Error types can also declare :http_status, :code, :severity, and
:retryable, so the boundary can act on the type rather than on a case that
has to know every error in the application. :redact keeps sensitive context out
of logs and JSON.
Where to go next
- Handling errors — the guards,
use Errata, and matching errors as values versus rescuing them as exceptions. - Wrapping and composing errors — wrapping a
lower-level failure as a
:cause, enriching context as an error travels, and aggregates that carry several errors at once. - Errors at a boundary — HTTP status, stable external codes, severity and retryability, normalizing foreign errors, and rendering an error for a user.
- Reporting errors —
Errata.log/2,Errata.report/2, the telemetry contract, and redaction. - Testing with Errata — where fixture types must live, asserting on errors readably, and the telemetry and log seams.
- Design notes — choosing a kind, choosing between a type and a reason, and why Errata works the way it does.
- Errata and AI coding agents — getting the rules this package ships in front of your agent.
Next: Handling errors