声明应用的错误词汇
将下面的模块放在 lib/ 中。各篇 guide 的 Error 定义都是独立示例;实际接入时应调整已有 catalog,不要重复定义同一个模块。
defmodule MyApp.Error do
use RintoError
catalog do
entry :payment_timeout,
as: :payment_unavailable,
http_status: 503,
doc: "The payment provider did not respond in time"
entry :payment_declined, http_status: 422
entry :reconciliation_failed
end
end
defmodule MyApp.Payments do
require MyApp.Error, as: Error
def interpret_result({:ok, receipt}, _order_id), do: {:ok, receipt}
def interpret_result({:error, :timeout}, order_id) do
{:error,
Error.new(
kind: :payment_timeout,
raw: :timeout,
data: %{order_id: order_id},
message: "Timed out while charging the order"
)}
end
def interpret_result({:error, :declined}, order_id) do
{:error, Error.new(kind: :payment_declined, data: %{order_id: order_id})}
end
end示例转换两种已知的支付服务结果。其他结果如何处理仍由应用明确决定,库不会凭空补出一个兜底 kind。
通过普通控制流返回错误
{:error, error} = MyApp.Payments.interpret_result({:error, :timeout}, "order-42")
error.kind # :payment_timeout
error.metadata.origin.module # MyApp.Payments
MyApp.Error.to_payload(error)
# %{kind: :payment_unavailable, data: %{order_id: "order-42"}, errors: []}调用方可以匹配 {:ok, receipt} 或 {:error, error},决定重试、响应或继续传递失败。返回 Error 不会自动打印日志或抛出异常。
new/2 是宏,调用模块需要 require MyApp.Error, as: Error。它将模块、函数和行号写入 metadata.origin,替换调用方传入的 origin。这里记录的是构造 Error 的位置,不一定是远端失败最初发生的位置。
选择合适的构造方式
下面每个构造表达式都返回 %MyApp.Error{}。为便于比较,使用 Map.take/2 展示关键字段的实际结果;其他字段使用默认值。所有 new 调用还会记录各自的 metadata.origin。
require MyApp.Error, as: Error
atom_error = Error.new(:payment_timeout)
Map.take(atom_error, [:kind, :raw, :message])
# %{kind: :payment_timeout, raw: :payment_timeout, message: nil}
message_error = Error.new(:payment_timeout, "Provider request timed out")
Map.take(message_error, [:kind, :raw, :message])
# %{kind: :payment_timeout, raw: :payment_timeout, message: "Provider request timed out"}
keyword_error = Error.new(kind: :payment_declined, data: %{order_id: "order-42"})
Map.take(keyword_error, [:kind, :raw, :data])
# %{kind: :payment_declined, raw: nil, data: %{order_id: "order-42"}}
map_error = Error.new(%{kind: :payment_timeout, raw: {:provider, :timeout}})
Map.take(map_error, [:kind, :raw])
# %{kind: :payment_timeout, raw: {:provider, :timeout}}
exception = Error.exception(kind: :payment_timeout)
Map.take(exception, [:kind, :raw, :metadata])
# %{kind: :payment_timeout, raw: nil, metadata: %{}}
literal = %Error{kind: :payment_timeout, raw: :timeout}
Map.take(literal, [:kind, :raw, :metadata])
# %{kind: :payment_timeout, raw: :timeout, metadata: %{}}Atom 输入同时设置 kind 和 raw;keyword 和 map 输入则允许分别设置它们,省略的 raw 为 nil。new 的第二个参数可以设置诊断消息。
exception/1 返回异常值,不会主动抛出,也不记录 origin。直接 struct 构造同样不记录 origin,并且绕过 catalog 校验,因此业务代码优先使用 new 或 exception。
raise MyApp.Error, ... 会构造并抛出异常,没有普通返回值。下面通过 rescue 展示捕获到的字段:
try do
raise MyApp.Error, kind: :payment_timeout
rescue
error in MyApp.Error -> Map.take(error, [:kind, :raw, :metadata])
end
# %{kind: :payment_timeout, raw: nil, metadata: %{}}已有错误时,raise error 直接抛出该错误,保留已有字段和 origin。预期业务失败通常通过 {:error, error} 返回。注册的 struct 也可以传给 new 或 exception,结果由 builder 决定,见后面的 Ecto 和自定义 struct guide。
在程序内部,可以充分利用 kind 和 raw 进行模式匹配:kind 表达统一的业务分类,raw 保留处理失败所需的原始信息。
case {:error, map_error} do
{:error, %MyApp.Error{kind: :payment_timeout, raw: {:provider, :timeout}}} ->
:retry_provider
{:error, %MyApp.Error{kind: :payment_declined}} ->
:ask_for_another_payment_method
other ->
other
end
# :retry_provider内部逻辑按存储的 kind 匹配;as: 只影响对外 payload 的名称。
用 typespec 表达错误约定
Error 模块会生成可直接引用的类型。业务函数的错误返回通常使用 MyApp.Error.t();明确知道 raw 的形状时,可以用 MyApp.Error.t(raw_type) 进一步描述。
defmodule MyApp.PaymentResult do
require MyApp.Error, as: Error
@spec from_timeout() :: {:error, Error.t(:timeout)}
def from_timeout do
{:error, Error.new(kind: :payment_timeout, raw: :timeout)}
end
@spec kind(Error.t()) :: Error.known_kind()
def kind(%Error{kind: kind}), do: kind
@spec payload(Error.t()) :: Error.payload()
def payload(error), do: Error.to_payload(error)
endError.t(:timeout) 约束的是 raw,不是 kind。Error.known_kind() 是 catalog 声明的内部 kind 联合类型;Error.payload() 描述公开输出。上例的 kind/1 假定输入来自经过校验的构造 API。
Typespec 为调用方和 Dialyzer 提供约定,不会增加运行时检查。Error.t() 中的 kind 类型仍是 atom();实际合法性由构造 API 按 catalog 验证。
将错误集中声明、管理和验证
这个库的设计目标,是让应用将接入的业务错误统一收敛到一个 Error 模块的 catalog 中。通过构造 API 创建的错误必须使用已声明的 kind,外部 struct 也必须显式注册,避免错误词汇散落在各个业务模块里。
这样,一处就能查看系统接入了哪些错误、如何对外命名,以及对应的 HTTP 状态和说明:kinds() 列出内部 kind,kinds(:full) 展示完整声明,kinds(:public) 按公开名称分组。
Catalog 声明会在编译时验证。直接传给 new 的未声明字面量 kind 会导致编译失败;动态输入和 source builder 返回的 kind 在构造时验证,不合法时抛出 ArgumentError。输出名称不会自动成为可构造的 kind。
这种强制约束作用于使用构造 API 接入的错误;库不会接管任意 Elixir 异常,也不阻止应用定义多个 Error 模块。直接 struct 构造或 map 更新会绕过校验,因此统一使用构造 API 才能持续维护这份集中声明。