Trogon.Error exception (Trogon.Error v0.6.0)

Copy Markdown View Source

Error Specification implementation for Elixir.

Quick Start

defmodule MyApp.NotFoundError do
  use Trogon.Error,
    domain: "com.myapp.resources",
    reason: "not_found",
    code: :NOT_FOUND,
    message: "Resource not found"
end

# Create an error instance with dynamic metadata
MyApp.NotFoundError.new!(
  metadata: Trogon.Error.Metadata.new(%{
    "resource_type" => "user",
    "resource_id" => "123"
  })
)

Design Philosophy

Trogon errors follow the "Error as Type" pattern where domain/0 + reason/0 uniquely identify the error type, message/0 is a static compile-time description, and metadata/0 carries instance-specific dynamic values.

The message is intentionally not overridable at runtime to ensure consistent messages for logging/monitoring and predictable error contracts for API consumers.

Key Types

Summary

Types

The standard error code. See Google RPC error codes for more details.

Debug information for troubleshooting, including stack traces and details.

The error domain identifying the service or component that generated the error.

Instance options for new!/1. See the type definition for available keys.

Help information containing links to documentation or support resources.

A single help link with a description and URL.

Unique identifier for this error instance.

Localized message for internationalization (i18n) support.

The error message template describing what went wrong.

Instance-specific metadata as key-value pairs.

A unique identifier for the specific error within the domain.

Retry information for recoverable errors, indicating when to retry.

Source identifier indicating where the error originated.

A pointer to the field or element that caused the error.

The Trogon error struct type, parameterized by the error module.

A single template option for defining an error module with use Trogon.Error.

Timestamp when the error occurred.

Whether the error should be visible to end users or kept internal.

Functions

See template_opt/0 for available options.

Guard that checks if a term is a Trogon error.

Creates a new Trogon error at runtime with dynamic values.

Converts a code atom or error struct to its integer value.

Converts a code atom or error struct to its HTTP status code.

Updates the help links for a Trogon error.

Types

code()

@type code() ::
  :CANCELLED
  | :UNKNOWN
  | :INVALID_ARGUMENT
  | :DEADLINE_EXCEEDED
  | :NOT_FOUND
  | :ALREADY_EXISTS
  | :PERMISSION_DENIED
  | :RESOURCE_EXHAUSTED
  | :FAILED_PRECONDITION
  | :ABORTED
  | :OUT_OF_RANGE
  | :UNIMPLEMENTED
  | :INTERNAL
  | :UNAVAILABLE
  | :DATA_LOSS
  | :UNAUTHENTICATED

The standard error code. See Google RPC error codes for more details.

debug_info()

@type debug_info() :: %{stack_entries: [String.t()], detail: String.t()}

Debug information for troubleshooting, including stack traces and details.

domain()

@type domain() :: String.t()

The error domain identifying the service or component that generated the error.

Examples: "com.myapp.payments", "com.stripe.api"

error_opt()

@type error_opt() ::
  {:metadata, metadata()}
  | {:causes, [t(module())]}
  | {:subject, subject() | nil}
  | {:debug_info, debug_info() | nil}
  | {:localized_message, localized_message() | nil}
  | {:retry_info, retry_info() | nil}
  | {:id, id() | nil}
  | {:time, time() | nil}
  | {:source_id, source_id() | nil}

Instance options for new!/1. See the type definition for available keys.

MyApp.NotFoundError.new!(
  metadata: Trogon.Error.Metadata.new(%{"user_id" => "123"}),
  subject: "user:123",
  localized_message: %{locale: "es", message: "Usuario no encontrado"}
)

The :message option is not supported

Use :localized_message for i18n or :metadata for dynamic values.

help()

@type help() :: %{links: [help_link()]}

Help information containing links to documentation or support resources.

help_link()

@type help_link() :: %{description: String.t(), url: String.t()}

A single help link with a description and URL.

id()

@type id() :: String.t()

Unique identifier for this error instance.

localized_message()

@type localized_message() :: %{locale: String.t(), message: String.t()}

Localized message for internationalization (i18n) support.

The locale follows IETF BCP-47 (e.g., "en-US", "fr-CH", "es-MX").

message()

@type message() :: String.t()

The error message template describing what went wrong.

Defined at compile time and cannot be overridden at runtime. For dynamic values, treat the message as a template and pass runtime data via :metadata.

metadata()

@type metadata() :: Trogon.Error.Metadata.t()

Instance-specific metadata as key-value pairs.

Trogon.Error.Metadata.new(%{
  "user_id" => "123",
  "resource_type" => "order"
})

See Trogon.Error.Metadata for visibility controls and advanced usage.

reason()

@type reason() :: String.t()

A unique identifier for the specific error within the domain.

Combined with domain/0, this creates a globally unique error identifier:

# domain: "com.myapp.payments", reason: "card_declined"
# domain: "com.myapp.users", reason: "not_found"

retry_info()

@type retry_info() :: retry_info_duration()

Retry information for recoverable errors, indicating when to retry.

retry_info: %{retry_offset: %Duration{second: 60}}

source_id()

@type source_id() :: String.t()

Source identifier indicating where the error originated.

subject()

@type subject() :: String.t()

A pointer to the field or element that caused the error.

Examples: "email", "user.address.zipCode", "/items/0/quantity" (JSON Pointer)

t(struct)

@type t(struct) :: %{
  __struct__: struct,
  __exception__: true,
  specversion: non_neg_integer(),
  code: code(),
  message: message(),
  domain: domain(),
  reason: reason(),
  metadata: metadata(),
  causes: [t(module())],
  visibility: visibility(),
  subject: subject() | nil,
  id: id() | nil,
  time: time() | nil,
  help: help() | nil,
  debug_info: debug_info() | nil,
  localized_message: localized_message() | nil,
  retry_info: retry_info() | nil,
  source_id: source_id() | nil
}

The Trogon error struct type, parameterized by the error module.

@spec find_user(String.t()) ::
        {:ok, User.t()}
        | {:error, Trogon.Error.t(MyApp.NotFoundError)}

template_opt()

@type template_opt() ::
  {:domain, domain()}
  | {:reason, reason()}
  | {:message, atom() | message()}
  | {:metadata, Trogon.Error.Metadata.raw()}
  | {:code, code()}
  | {:visibility, visibility()}
  | {:help, help()}

A single template option for defining an error module with use Trogon.Error.

defmodule MyApp.NotFoundError do
  use Trogon.Error,
    domain: "com.myapp.resources",
    reason: "not_found",
    code: :NOT_FOUND,
    message: "Resource not found"
end

Options

time()

@type time() :: DateTime.t()

Timestamp when the error occurred.

visibility()

@type visibility() :: :INTERNAL | :PRIVATE | :PUBLIC

Whether the error should be visible to end users or kept internal.

  • :INTERNAL - The error is not visible from outside the application.
  • :PRIVATE - The error is visible from applications belonging to the same organization.
  • :PUBLIC - The error is visible to everyone.

Functions

__using__(opts)

(macro)
@spec __using__([template_opt()]) :: Macro.t()

See template_opt/0 for available options.

is_trogon_error?(term)

(macro)

Guard that checks if a term is a Trogon error.

Examples

iex> error = TestSupport.TestError.new!()
iex> require Trogon.Error
iex> Trogon.Error.is_trogon_error?(error)
true

iex> require Trogon.Error
iex> Trogon.Error.is_trogon_error?(%{})
false

metadata()

See Trogon.Error.Metadata.new/0.

new!(opts)

@spec new!([template_opt() | error_opt()]) :: t(Trogon.Error)

Creates a new Trogon error at runtime with dynamic values.

Useful for handling external errors from services you don't control, without needing predefined error modules.

Examples

# Basic external error
Trogon.Error.new!(
  domain: "com.stripe.payment",
  reason: "card_declined",
  message: "Your card was declined"
)

# With additional metadata and options
Trogon.Error.new!(
  domain: "com.external.api",
  reason: "rate_limit_exceeded",
  code: :RESOURCE_EXHAUSTED,
  message: "Rate limit exceeded",
  metadata: Trogon.Error.Metadata.new(%{
    "limit" => "100",
    "window" => "3600"
  }),
  subject: "api-client-123",
  retry_info: %{retry_offset: %Duration{second: 60}}
)

to_code_int(arg1)

@spec to_code_int(atom() | t(module())) :: non_neg_integer()

Converts a code atom or error struct to its integer value.

Examples

iex> Trogon.Error.to_code_int(:CANCELLED)
1

iex> err = TestSupport.InvalidCurrencyError.new!()
iex> Trogon.Error.to_code_int(err)
2

to_http_status_code(arg1)

@spec to_http_status_code(atom() | t(module())) :: non_neg_integer()

Converts a code atom or error struct to its HTTP status code.

Examples

iex> Trogon.Error.to_http_status_code(:CANCELLED)
499

iex> err = TestSupport.InvalidCurrencyError.new!()
iex> Trogon.Error.to_http_status_code(err)
500

to_msg(msg)

@spec to_msg(atom() | String.t()) :: String.t()

update_help_links(error, updater)

@spec update_help_links(t(module()), (t(module()), [help_link()] -> [help_link()])) ::
  t(module())

Updates the help links for a Trogon error.

The updater receives the error and the current links list, normalized from nil to [], and must return the replacement links list.

Examples

iex> defmodule MyApp.Error.InsufficientInventoryError do
...>   use Trogon.Error,
...>     domain: "com.myapp.inventory",
...>     reason: "INSUFFICIENT_INVENTORY"
...> end
iex> updater_links = fn err, links, principal_role ->
...>   path = "/runbooks/" <> err.domain <> "/" <> err.reason <> "?role=#{principal_role}"
...>   url = "https://docs.example.com" <> path
...>   links ++ [%{description: "Runbook", url: url}]
...> end
iex> err = MyApp.Error.InsufficientInventoryError.new!()
iex> err = Trogon.Error.update_help_links(err, &updater_links.(&1, &2, :ai_agent))
iex> err.help
%{links: [
  %{description: "Runbook", url: "https://docs.example.com/runbooks/com.myapp.inventory/INSUFFICIENT_INVENTORY?role=ai_agent"}
]}