You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Plug的Elixir REST API自定义MatchError优雅处理咨询

好问题!在Elixir/Plug里处理这类业务级的匹配错误,确实不能直接依赖默认的MatchError——它属于系统级异常,会被当成500错误返回,完全没法传递自定义业务信息。下面给你几个简洁的实现思路,完全能满足你返回自定义错误的需求:

1. 先定义专属的业务异常模块

首先我们要把业务错误和系统错误区分开,创建一个自定义异常:

defmodule MyApp.BusinessError do
  defexception [:code, :reason, :status]

  # 定义异常的默认消息,方便调试
  def message(%__MODULE__{code: code, reason: reason}) do
    "Business Error #{code}: #{reason}"
  end
end

这里给异常加上了code(业务错误码)、reason(错误描述)和status(HTTP状态码,比如422表示请求语义错误)三个字段,完全贴合你的需求。

2. 替换直接模式匹配,改用显式判断+抛自定义异常

你原来的0 = DbAdapter.select(...)会直接抛出MatchError,我们改成显式判断结果,然后抛出自己定义的业务异常:

# 原来的写法(会触发MatchError)
# 0 = DbAdapter.select("SELECT * FROM users WHERE email = 'email@example.com';")

# 改进后的写法(建议用参数化查询避免SQL注入)
case DbAdapter.select("SELECT * FROM users WHERE email = $1;", [email]) do
  [] -> :ok  # 没找到用户,继续创建流程
  _users ->
    raise MyApp.BusinessError, 
          code: 1113, 
          reason: "account already exists", 
          status: 422
end

3. 在Plug的handle_errors中捕获自定义异常

现在修改全局的错误处理函数,匹配我们的业务异常,返回格式化后的响应:

def handle_errors(conn, %{reason: %MyApp.BusinessError{code: code, reason: reason, status: status}}) do
  # 用Jason把错误序列化成JSON返回(确保你已经添加了Jason依赖)
  error_body = Jason.encode!(%{code: code, message: reason})
  send_resp(conn, status, error_body)
end

# 保留兜底的系统错误处理
def handle_errors(conn, _error_details) do
  default_error = Jason.encode!(%{code: 500, message: "Internal server error"})
  send_resp(conn, 500, default_error)
end

更优雅的进阶写法:用with串联业务逻辑

如果你的创建用户流程有多个步骤(比如参数验证、邮箱检查、插入数据库),用with可以让代码更扁平清晰,甚至不用走到全局的handle_errors:

def create_user(conn, params) do
  with {:ok, email} <- validate_email(params),
       :ok <- check_email_not_exists(email),
       {:ok, user} <- insert_user(params) do
    # 成功流程
    send_resp(conn, 201, Jason.encode!(%{data: user}))
  else
    {:error, %MyApp.BusinessError{} = err} ->
      # 直接在业务函数内返回自定义错误响应
      error_body = Jason.encode!(%{code: err.code, message: err.reason})
      send_resp(conn, err.status, error_body)
    {:error, reason} ->
      # 处理其他非业务类错误
      send_resp(conn, 400, Jason.encode!(%{code: 400, message: reason}))
  end
end

defp check_email_not_exists(email) do
  case DbAdapter.select("SELECT * FROM users WHERE email = $1;", [email]) do
    [] -> :ok
    _ -> {:error, %MyApp.BusinessError{code: 1113, reason: "account already exists", status: 422}}
  end
end

小提示:别用模式匹配做业务判断

Elixir的模式匹配更适合结构匹配(比如匹配函数返回的{:ok, data}或{:error, reason}),而业务级的存在性判断,用case显式处理会更清晰,也更容易维护。

内容的提问来源于stack exchange,提问作者widavies

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 08:33:48