Declare the application's error vocabulary
Put these modules under lib/. Each guide's Error definition is a standalone example; adapt your existing catalog instead of defining the same module twice.
defmodule MyApp.Error do
use RintoError
catalog do
entry :payment_timeout,
as: :payment_unavailable,
http_status: 503,
doc: "The payment provider did not respond in time"
entry :payment_declined, http_status: 422
entry :reconciliation_failed
end
end
defmodule MyApp.Payments do
require MyApp.Error, as: Error
def interpret_result({:ok, receipt}, _order_id), do: {:ok, receipt}
def interpret_result({:error, :timeout}, order_id) do
{:error,
Error.new(
kind: :payment_timeout,
raw: :timeout,
data: %{order_id: order_id},
message: "Timed out while charging the order"
)}
end
def interpret_result({:error, :declined}, order_id) do
{:error, Error.new(kind: :payment_declined, data: %{order_id: order_id})}
end
endThis example translates two known provider outcomes. Other outcomes remain an explicit application decision; the library does not invent a fallback kind.
Return errors through ordinary control flow
{:error, error} = MyApp.Payments.interpret_result({:error, :timeout}, "order-42")
error.kind # :payment_timeout
error.metadata.origin.module # MyApp.Payments
MyApp.Error.to_payload(error)
# %{kind: :payment_unavailable, data: %{order_id: "order-42"}, errors: []}The caller can pattern-match on {:ok, receipt} or {:error, error} and decide whether to retry, respond, or propagate the failure. Returning an Error does not log or raise it.
new/2 is a macro, so the calling module must require MyApp.Error, as: Error. It records the module, function, and line in metadata.origin, replacing a caller-supplied origin. This location is where the Error is constructed, not necessarily where a remote failure originally occurred.
Choose the construction form
Each construction expression below returns %MyApp.Error{}. For comparison, Map.take/2 shows the actual values of selected fields; other fields keep their defaults. Every new call also records its own metadata.origin.
require MyApp.Error, as: Error
atom_error = Error.new(:payment_timeout)
Map.take(atom_error, [:kind, :raw, :message])
# %{kind: :payment_timeout, raw: :payment_timeout, message: nil}
message_error = Error.new(:payment_timeout, "Provider request timed out")
Map.take(message_error, [:kind, :raw, :message])
# %{kind: :payment_timeout, raw: :payment_timeout, message: "Provider request timed out"}
keyword_error = Error.new(kind: :payment_declined, data: %{order_id: "order-42"})
Map.take(keyword_error, [:kind, :raw, :data])
# %{kind: :payment_declined, raw: nil, data: %{order_id: "order-42"}}
map_error = Error.new(%{kind: :payment_timeout, raw: {:provider, :timeout}})
Map.take(map_error, [:kind, :raw])
# %{kind: :payment_timeout, raw: {:provider, :timeout}}
exception = Error.exception(kind: :payment_timeout)
Map.take(exception, [:kind, :raw, :metadata])
# %{kind: :payment_timeout, raw: nil, metadata: %{}}
literal = %Error{kind: :payment_timeout, raw: :timeout}
Map.take(literal, [:kind, :raw, :metadata])
# %{kind: :payment_timeout, raw: :timeout, metadata: %{}}Atom input sets both kind and raw. Keyword and map inputs let you set them separately, with omitted raw defaulting to nil. The second argument to new can supply a diagnostic message.
exception/1 returns an exception value without raising or recording an origin. Direct struct construction also omits the origin and bypasses catalog validation, so prefer new or exception in business code.
raise MyApp.Error, ... constructs and raises an exception, with no normal return value. This example uses rescue to show the captured fields:
try do
raise MyApp.Error, kind: :payment_timeout
rescue
error in MyApp.Error -> Map.take(error, [:kind, :raw, :metadata])
end
# %{kind: :payment_timeout, raw: nil, metadata: %{}}For an existing error, raise error raises that value, preserving its fields and origin. Expected business failures usually travel as {:error, error}. Registered structs can also be passed to new or exception; their builders determine the result, as shown in the Ecto and custom struct guides.
Inside the application, use both kind and raw for pattern matching: kind provides a shared business classification, while raw retains the original information needed to handle the failure.
case {:error, map_error} do
{:error, %MyApp.Error{kind: :payment_timeout, raw: {:provider, :timeout}}} ->
:retry_provider
{:error, %MyApp.Error{kind: :payment_declined}} ->
:ask_for_another_payment_method
other ->
other
end
# :retry_providerInternal logic matches the stored kind; as: only changes its name in the public payload.
Describe error contracts with typespecs
The Error module generates types that your application can reference. Use MyApp.Error.t() for ordinary error returns, or MyApp.Error.t(raw_type) when you know the shape of raw.
defmodule MyApp.PaymentResult do
require MyApp.Error, as: Error
@spec from_timeout() :: {:error, Error.t(:timeout)}
def from_timeout do
{:error, Error.new(kind: :payment_timeout, raw: :timeout)}
end
@spec kind(Error.t()) :: Error.known_kind()
def kind(%Error{kind: kind}), do: kind
@spec payload(Error.t()) :: Error.payload()
def payload(error), do: Error.to_payload(error)
endError.t(:timeout) constrains raw, not kind. Error.known_kind() is the union of internal kinds declared in the catalog; Error.payload() describes public output. The example's kind/1 assumes input from the validated construction API.
Typespecs communicate contracts to callers and Dialyzer; they add no runtime checks. The kind field in Error.t() remains atom(). The construction API validates its actual value against the catalog.
Centralize, inspect, and validate errors
The library is designed to bring the application's integrated business errors together in one Error module's catalog. Errors created through the construction API must use declared kinds, and external structs must be explicitly registered. This keeps the error vocabulary from being scattered across business modules.
That catalog provides one place to inspect the integrated errors, their public names, HTTP statuses, and descriptions: kinds() lists internal kinds, kinds(:full) shows complete declarations, and kinds(:public) groups them by public name.
Catalog declarations are validated at compile time. An undeclared literal kind passed directly to new fails compilation; dynamic inputs and kinds returned by source builders are validated during construction and raise ArgumentError when invalid. Output names do not automatically become constructable kinds.
This enforcement applies to errors integrated through the construction API. The library does not take over arbitrary Elixir exceptions or prevent multiple Error modules. Direct struct construction and map updates bypass validation, so consistently using the construction API maintains the centralized contract.
中文 · Previous: Design and error model · Next: Ecto validation errors