View Source Errata.Aggregate (Errata v1.9.0)

Errors that hold several other errors.

Validation is the shape this exists for: a request fails and there are five reasons, all of which the caller needs. Modelling that as one error with a list of maps in :context throws away everything Errata is for — each sub-failure loses its type, code, HTTP status, severity, and retryability, and becomes inert data.

An aggregate type keeps them as errors:

defmodule MyApp.Orders.ValidationFailed do
  use Errata.DomainError, aggregate: true
end

ValidationFailed.new(errors: [email_error, age_error])

The aggregate is itself an ordinary Errata error — Errata.is_error/1 holds, it can be raised and returned in an {:error, _} tuple, and it serializes through to_map/1 and the JSON encoders like any other. Its members serialize with it, each keeping its own type, code, and redaction rules.

Members must be Errata errors

Every member has to satisfy Errata.is_error/1; anything else raises ArgumentError when the aggregate is built. This is deliberate rather than incidental: the merge rules below are defined in terms of severity/1, retryable?/1, and http_status/1, and a bare map or a foreign exception cannot answer them. Wrap a foreign error in an Errata type first — that is what Errata.wrap/3 is for.

Merge rules

An aggregate has to answer severity/1, retryable?/1, and http_status/1 for a collection rather than one failure. The three do not merge the same way, because the right answer differs:

  • severity/1 — the most severe member. Severities are totally ordered, so the maximum is unambiguous, and it is what a log level should be: if any member is :error, the aggregate is at least :error. Anything less would under-report a real failure.

  • retryable?/1 — retryable only if every member is. Retrying the aggregate helps only if all of it could succeed next time. A single permanent failure makes the retry pointless, so the conservative reading is the correct one.

  • http_status/1 — the members' status if they all agree, otherwise the aggregate's own. There is no meaningful maximum over status codes: 503 is numerically greater than 500 but that ordering means nothing, so picking a "highest" would be arbitrary. Unanimity is the only member-derived answer that is never wrong, and for a heterogeneous bag the container's declared status is the honest one.

An aggregate with no members falls back to its own declared values for all three, since there is nothing to merge.

Each of the three remains overridable per type, so a type that wants different rules — first member wins, say, or a fixed status regardless — just defines the function.

Summary

Functions

Appends a summary of errors to an aggregate's own message.

Returns the HTTP status shared by every error in errors, or fallback when they disagree or the list is empty.

Returns true only when every error in errors is retryable; fallback when empty.

Returns the most severe severity among errors, or fallback when empty.

Raises ArgumentError unless every member of errors is an Errata error.

Functions

Link to this function

format_message(message, errors)

View Source
@spec format_message(String.t(), [Errata.error()]) :: String.t()

Appends a summary of errors to an aggregate's own message.

The member messages are the actionable part of an aggregate — "validation failed" alone tells you nothing — so they are rendered inline rather than left for to_map/1 to carry.

Link to this function

http_status(errors, fallback)

View Source
@spec http_status([Errata.error()], non_neg_integer()) :: non_neg_integer()

Returns the HTTP status shared by every error in errors, or fallback when they disagree or the list is empty.

Link to this function

retryable?(errors, fallback)

View Source
@spec retryable?([Errata.error()], boolean()) :: boolean()

Returns true only when every error in errors is retryable; fallback when empty.

Link to this function

severity(errors, fallback)

View Source
@spec severity([Errata.error()], Logger.level()) :: Logger.level()

Returns the most severe severity among errors, or fallback when empty.

Link to this function

validate_members!(errors, error_type)

View Source
@spec validate_members!(term(), module()) :: [Errata.error()]

Raises ArgumentError unless every member of errors is an Errata error.

Returns the list unchanged when valid.