View Source Handling errors
Errata errors are standard Elixir exceptions, so they can be rescued like any
other exception, and Kernel.is_exception/1 returns true for them. In
addition, Errata provides guards for recognizing and classifying its errors:
Errata.is_error/1— true for any Errata errorErrata.is_domain_error/1— true for domain errorsErrata.is_infrastructure_error/1— true for infrastructure errors
Because the guards are macros, the Errata module must be required or
imported to use them. The simplest way is use Errata, which imports the three
guards — so you can write them unqualified in when clauses and function heads —
and, because import implies require, also makes the Errata.create/2 and
Errata.wrap/3 macros callable:
defmodule MyApp.Orders.Boundary do
use Errata
def handle({:error, e}) when is_error(e), do: handle_errata_error(e)
def handle({:error, e}), do: handle_other_error(e)
enduse Errata brings only the guards into scope; the rest of the API stays
qualified (Errata.to_map/1, Errata.put_context/3, and so on), which reads
well at a boundary and avoids pulling generically named functions into your
namespace. (Don't confuse it with use Errata.Error and friends, which define
a new error type.) If you'd rather not use the module, the explicit form
imports just the guards — which, again, also requires the module:
import Errata, only: [is_error: 1, is_domain_error: 1, is_infrastructure_error: 1]The two differ in one respect. Elixir warns about an unused import written out
like this, but not about one a macro generated, so a module that only creates
errors and never calls a guard compiles cleanly with use Errata and warns with
the explicit import. That is why use Errata is the recommended setup line for
every module that touches Errata errors — whether it creates them, handles them,
or both — and the explicit form suits a module that does call the guards and
prefers to say so.
The kind-based guards are especially useful at system boundaries — for example,
translating domain errors into client errors (4xx) and infrastructure errors
into server errors (5xx) with alerting — while domain logic generally matches
on the specific error type.
rescueclauses and the custom guardsElixir's
rescueclauses only accept a bare variable or thevar in [ExceptionModule]form; they do not accept arbitrarywhenguards. To use theErrata.is_error/1family when rescuing, rescue the exception into a variable and then dispatch on it (for example withcond/1), as shown below. The guards can be used directly in thewhenclause of acase,with, or function head when handling errors returned as values.
The following example handles Errata errors both as raised exceptions and as error values returned from functions:
defmodule MyApp.Orders.Boundary do
use Errata
def handle_order_lookup_as_exception(id) do
try do
MyApp.Orders.fetch_order!(id)
rescue
e in [MyApp.Orders.OrderNotFound] ->
# Errata errors can be rescued by their specific type
handle_order_not_found(e)
e ->
# `rescue` clauses cannot use `when` guards, so rescue the exception
# and then dispatch on it using the guards
cond do
is_error(e) -> handle_errata_error(e)
# Regular exceptions may be handled separately if desired
true -> handle_other_error(e)
end
end
end
def handle_order_lookup_as_value(id) do
case MyApp.Orders.fetch_order(id) do
{:ok, order} ->
handle_order(order)
{:error, %MyApp.Orders.OrderNotFound{} = error} ->
# Errata errors can be pattern matched by their specific type
handle_order_not_found(error)
{:error, error} when is_error(error) ->
# Or they can be identified using one of the guards (`when` guards are
# allowed in `case` clauses)
handle_errata_error(error)
{:error, reason} ->
# Other errors may be handled separately if desired
handle_other_error(reason)
end
end
endThe patterns above, distilled into runnable examples — first, rescuing an exception and dispatching on it with the custom guards:
iex> require Errata
iex> alias MyApp.Orders.{OrderNotFound, PaymentDeclined}
iex> try do
...> raise OrderNotFound, reason: :not_found
...> rescue
...> e in [PaymentDeclined] ->
...> {:specific, e.reason}
...>
...> e ->
...> # `Errata.reason/1` rather than `e.reason`: a variable bound by a bare
...> # `rescue e ->` has no type the compiler can narrow (see the note below).
...> if Errata.is_error(e), do: {:errata, Errata.reason(e)}, else: {:other, e}
...> end
{:errata, :not_found}And second, matching on an error returned as a value, where the guards can be
used directly in a when clause:
iex> require Errata
iex> alias MyApp.Orders.OrderNotFound
iex> case {:error, OrderNotFound.new(reason: :not_found)} do
...> {:error, e} when Errata.is_error(e) -> {:errata, e.reason}
...> {:error, other} -> {:other, other}
...> end
{:errata, :not_found}Reading fields inside a bare
rescueA variable bound by a bare
rescue e ->has no type the compiler can narrow — it is "some exception, fields unknown" — so reading a field directly withe.reasondraws anunknown key .reasonwarning. This is ordinary Elixir behaviour rather than anything about Errata:e.messageon a plainRuntimeErrorwarns in exactly the same position.Use an accessor instead.
Errata.reason/1,Errata.context/1,Errata.kind/1,Errata.code/1,Errata.severity/1,Errata.http_status/1,Errata.retryable?/1, andErrata.cause/1are plain function calls, so they warn for nothing and read better than field access besides. Or match the specific type —e in [PaymentDeclined] -> e.reason— when you know it.Field access after a structural guard (
{:error, e} when Errata.is_error(e)) is warning-free — verified on every Elixir this library supports, 1.15 through 1.20.
Previous: Getting started · Next: Wrapping and composing errors