View Source Errata.Error behaviour (Errata v1.9.0)
Support for creating custom error types, which can either be returned as error values or raised as exceptions.
Errata errors can be defined by creating an Elixir module that uses the Errata.Error
module. Error types defined in this way are Elixir Exception structs with the following keys:
message- human readable string describing the nature of the errorreason- an atom describing the reason for the error, which can be used for pattern matching or classifying the errorcontext- a map containing arbitrary contextual information or metadata about the error
Note the distinction between two ways of rendering an error as a string.
Exception.message/1 (and the String.Chars implementation) return a
developer-oriented message that combines message and reason (for
example, "the requested order does not exist: :not_found") — useful in logs
and raised-exception output. Errata.display_message/1 returns just the
human-readable message, intended for rendering to end users.
Because these error types are defined with defexception/1, they can be raised as exceptions
with raise/2. However, because they implement the Errata.Error behaviour, it is also
possible to create instances of these error structs using the generated implementations of
Errata.Error.new/1 or Errata.Error.create/1 and use them as return values from
functions, either directly or wrapped in an error tuple such as {:error, my_error}.
Error types defined with Errata.Error are of kind :general by default. Since most errors
are either domain errors or infrastructure errors, prefer Errata.DomainError or
Errata.InfrastructureError (which share all of the functionality described here) when
defining custom error types, and use Errata.Error directly only for general errors that fit
neither category, such as errors originating in library code.
Usage
To define a new custom error type, use/2 the Errata.Error module in your own error module:
defmodule MyApp.UnexpectedError do
use Errata.Error,
default_message: "an unexpected error occurred"
end
use Errata.ErrorWhen you
use Errata.Error, theErrata.Errormodule will define an exception struct withdefexception/1and will generate an implementation of theErrata.Errorbehaviour.
The following options may be provided to use Errata.Error. The list is closed: an option that
is misspelled or unrecognized raises ArgumentError at compile time rather than being silently
ignored, since a use option is written once and a misconfiguration would otherwise be permanent
and invisible.
:default_reason- the default value to use for the:reasonfield if it is not provided:default_message- the default value to use for the:messagefield if it is not provided. This is a static string; to compute a user-facing message from the error's:reasonor:context(naming the particular order or item, say), override the generateddisplay_message/1function instead —Errata.display_message/1andto_map/1both dispatch through it. SeeErrata.display_message/1.A type that declares no
:default_messagerenders asnilthrough every display path. To give every such type a floor rather than repeating a fallback at each boundary, set an application-wide default:config :errata, default_display_message: "an unexpected error occurred"It applies only where the type declares nothing and the caller passed no
:message, and defaults tonil, which is the historical behaviour. This mirrorsconfig :errata, redact:— a global floor that individual types refine.:reasons- an optional list of atoms enumerating the valid reasons for this error type. When given, creating an error (vianew/1,create/1,wrap/2, orraise/2) with a:reasonoutside this set raises anArgumentError. Anil(unspecified) reason is always allowed, and a:default_reason, if also given, must be one of the declared:reasons. Declaring reasons also generates areason/0type enumerating them, so the valid reasons are visible in the generated documentation.:http_status- the HTTP status code to associate with this error type, returned by the generatedhttp_status/1function (andErrata.http_status/1). When omitted, the status defaults off the error's kind (:domain→422,:infrastructure→503,:general→500). The generatedhttp_status/1is overridable, so it can instead be defined to compute a status from the error's:reasonor:context.:code- a stable external code for this error type (such as"ORDER_NOT_FOUND"), returned by the generatedcode/1function (andErrata.code/1) and included into_map/1. A code is independent of the module name, so it remains a valid contract with external consumers even if the module is renamed or moved. There is no default: types that do not declare one returnnil. The generatedcode/1is overridable, so it can instead be defined to derive a code from the error's:reasonor:context.:severity- the severity of this error type, as aLogger.level/0, returned by the generatedseverity/1function (andErrata.severity/1). Defaults to:errorfor every kind. This is the level at whichErrata.log/2logs the error when no level is given explicitly, and it is included in the metadata emitted byErrata.log/2andErrata.report/2. The generatedseverity/1is overridable, so it can instead be defined to compute a severity from the error's:reasonor:context.:retryable- whether errors of this type are retryable, returned by the generatedretryable?/1function (andErrata.retryable?/1). When omitted, this defaults off the error's kind::infrastructureerrors are retryable,:domainand:generalerrors are not. The generatedretryable?/1is overridable, so it can instead be defined to decide from the error's:reasonor:context.:redact- a list of context keys whose values are sensitive, replaced with"[REDACTED]"everywhere Errata serializes the context:to_map/1and the JSON encoding,Errata.log/2metadata, andErrata.report/2telemetry metadata. Redaction is recursive and matches atom and binary keys alike, soredact: [:password]covers a password nested inside a captured params map with string keys. The error struct keeps the real values, so they remain available locally for debugging. Defaults to[]; add a global floor withconfig :errata, redact: [...]. The generatedredact_context/1is overridable for rules a key list can't express. SeeErrata.Redaction.:aggregate- whentrue, this type can hold member errors, for the "several things went wrong at once" shape that validation produces. Adds an:errorsfield (a list of Errata errors, empty by default) thatnew/1andcreate/1accept, includes the members into_map/1and the message, and mergesseverity/1,retryable?/1, andhttp_status/1across them — each by a different rule, and each still overridable. Members must themselves be Errata errors. Defaults tofalse. SeeErrata.Aggregate.:kind- the "kind" of Errata error to create, one of:domain,:infrastructure, or:general(which is the default). Accepted only here:use Errata.DomainErroranduse Errata.InfrastructureErrorset the kind themselves and reject the option.
The
:kindoptionAlthough it is possible to define domain error types or infrastructure error types by using
:domainor:infrastructureas the:kindoption, it is preferred to instead define these types of errors withuse Errata.DomainErrororuse Errata.InfrastructureError. This approach is more explicit and allows for easier identification of domain errors and infrastructure errors within an application.
To create instances of the error--to use as an error return value from a function, say--the
recommended path is Errata.create/2, which captures the current __ENV__ and stacktrace into
the :env field. Because it takes the error type as an argument, a single use Errata covers
every error type the module creates, with no per-type require:
defmodule MyApp.SomeModule do
use Errata
alias MyApp.UnexpectedError
def some_function(arg) do
{:error, Errata.create(UnexpectedError, reason: :unexpected, context: %{arg: arg})}
end
endThe generated create/1 does the same thing and reads more directly when a module works mostly
with one error type, at the cost of a require for that module, since the callback is implemented
as a macro:
defmodule MyApp.SomeModule do
require MyApp.UnexpectedError, as: UnexpectedError
def some_function(arg) do
{:error, UnexpectedError.create(reason: :unexpected, context: %{arg: arg})}
end
endnew/1 is a plain function that builds the error without environment info. See new/1 for
when that is the right choice.
To raise errors as exceptions, simply use raise/2 passing extra params as the second argument
if desired:
defmodule MyApp.SomeModule do
require MyApp.UnexpectedError, as: UnexpectedError
def some_function!(arg) do
raise UnexpectedError, reason: :unexpected, context: %{arg: arg}
end
end
The generated t/0 type
Every generated error type gets a t/0 type, but it is the kind-level type
rather than one naming the struct:
@type t() :: Errata.domain_error()So every domain error type has a literally identical t/0, and a spec written as
@spec refund(Order.t(), PaymentDeclined.t()) :: :ok accepts any domain error.
To write a spec that names one error type, use the struct form instead:
@spec refund(Order.t(), %PaymentDeclined{}) :: :okThis is the opposite of the usual Elixir convention, where t/0 means "this
module's type", so it is worth knowing which of the two you are reaching for.
The reason/0 type generated from :reasons is specific — it enumerates the
declared values — so a spec written against PaymentDeclined.reason() gets real
checking.
Dialyzer's :extra_return flag
The generated http_status/1, code/1, severity/1 and retryable?/1 carry
behaviour-level specs while their default bodies return a compile-time literal.
A type declaring code: "ORDER_NOT_FOUND" therefore has success typing
<<_::176>> against a spec that also admits nil, and one that never overrides
retryable?/1 has success typing false against boolean(). With
flags: [:extra_return], Dialyzer reports an extra_range warning for each,
and the count grows with every error type an application defines.
Narrowing the specs per type would trade this warning for a worse one: a type
declared retryable: false would get @spec retryable?(...) :: false, and the
first override returning true for a particular reason — the extension point
these functions exist for — would then be the thing Dialyzer flagged, in user
code. :extra_return is best left off in a project that uses Errata.
Summary
Types
Type to represent allowable keys to use in params used for creating error structs.
Type to represent allowable values to be passes as params for creating error structs.
Type to represent Errata error structs.
Callbacks
Invoked to create a new instance of an error struct with default values and the current
__ENV__.
Invoked to create a new instance of an error struct with the given params and the current
__ENV__.
Invoked to create a new instance of an error struct with default values.
Invoked to create a new instance of an error struct with the given params.
Invoked to convert an error to a plain, JSON-encodable map.
Invoked to wrap an existing error, exception, or arbitrary value as the
:cause of a new error struct, capturing the current __ENV__.
Invoked to wrap an existing error as the :cause of a new error struct, with
the given opts, capturing the current __ENV__.
Types
@type param() :: :message | :reason | :context | :cause
Type to represent allowable keys to use in params used for creating error structs.
See also params/0.
@type params() :: Enumerable.t({param(), any()})
Type to represent allowable values to be passes as params for creating error structs.
This effectively allows for using either a map or keyword list with allowable keys defined by
param/0.
@type t() :: Errata.error()
Type to represent Errata error structs.
Error structs are Exception structs that have additional fields to contain extra contextual
information, such as an error reason or details about the context in which the error occurred.
Callbacks
@macrocallback create() :: Macro.t()
Invoked to create a new instance of an error struct with default values and the current
__ENV__.
See create/1.
Invoked to create a new instance of an error struct with the given params and the current
__ENV__.
Since this is a macro, the __ENV__/0 special form is used to capture the Macro.Env struct
for the current environment and the public fields of this struct are placed in the exception
struct under the :env key. This provides access to information about the context in which the
error was created, such as the module, function, file, and line. See t:env/0 for further
details.
Note that because this is a macro, callers must require/2 the error module to be able to use it.
Errata.create/2 avoids that per-module require — it takes the error type as an argument, so a
single use Errata (or require Errata) covers every error type a module creates, with the same
:env capture. Prefer it when a module works with several error types.
Capturing the environment walks the process stack, which costs on the order of a microsecond per
error — negligible against almost any operation that can fail, including in with chains at
request volume. The stacktrace is already capped by the VM (8 frames by default), so the cost does
not grow with stack depth. Reach for new/1 only when you need a plain function, not to avoid
this cost.
@callback new() :: t()
Invoked to create a new instance of an error struct with default values.
See new/1.
Invoked to create a new instance of an error struct with the given params.
Unlike create/1, this leaves the :env field nil: it records nothing
about where the error was created. Prefer create/1 or Errata.create/2
unless you need one of the things a macro cannot do — calling it dynamically
with apply/3, or capturing it as &SomeError.new/1 to pass around. It is
also convenient in tests and fixtures, where env: nil keeps error structs
easy to compare.
Invoked to convert an error to a plain, JSON-encodable map.
Invoked to wrap an existing error, exception, or arbitrary value as the
:cause of a new error struct, capturing the current __ENV__.
This is the idiomatic way to translate a lower-level failure into a structured
Errata error without losing the context of the original. It is equivalent to
create/1 with the given cause placed in the :cause field. See
wrap/2 to also provide params (such as a :reason) and the original
stacktrace.
Like create/1, this is a macro, so callers must require/2 the error module.
Invoked to wrap an existing error as the :cause of a new error struct, with
the given opts, capturing the current __ENV__.
In addition to the standard params accepted by create/1 (:message,
:reason, :context), opts may include:
:stacktrace- the stacktrace where the original error occurred, typically__STACKTRACE__from within arescue/catchclause:kind- the kind of the wrapped error, one of:error(the default),:throw, or:exit
The wrapped value is stored as an Errata.Cause in the :cause field, and can
be retrieved with Errata.cause/1. The typical use is to translate a rescued
exception while preserving its original stacktrace:
try do
Jason.decode!(payload)
rescue
e ->
{:error, MyApp.InvalidPayload.wrap(e, stacktrace: __STACKTRACE__, reason: :malformed_json)}
endLike create/1, this is a macro, so callers must require/2 the error module.