View Source Errata.Error behaviour (Errata v1.7.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:
: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.: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)
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
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.