Ecto与Phoenix为何用异常处理常规错误?如何优雅应对?
首先得明确Elixir生态里带!后缀函数的设计逻辑:这类函数是为简化「预期成功」场景的代码而生的。对应的非!版本(比如get_admin_user/1)会返回{:ok, 结果}或{:error, 原因}的元组,要求开发者显式处理所有分支;而!版本则在成功时直接返回结果,失败时抛出异常——这并没有违背「异常仅用于异常情况」的原则,框架默认这么设计的原因是:
- 在Web场景中,比如后台管理页面,用户请求一个不存在的管理员ID,本质属于客户端错误,而非应用正常业务流程的一部分。这种情况不需要在每个业务逻辑里重复处理,统一返回404响应即可。
!版本能减少冗余代码:如果每个action都要写模式匹配处理{:error, :not_found},会增加大量重复代码,而Phoenix的全局异常捕获机制可以统一接管这类情况。
根据不同场景,有几种实用方案:
依赖Phoenix全局异常处理
Phoenix默认会捕获Ecto.NoResultsError(也就是get_admin_user!/1找不到数据时抛出的异常),并通过error_view.ex返回404响应。你还可以自定义这个逻辑,比如在lib/your_app_web/views/error_view.ex里添加:def render("404.html", _assigns) do "管理员不存在" end def render("404.json", _assigns) do %{error: "管理员不存在"} end这样所有因
!函数抛出的未找到异常,都会自动返回对应的404页面或JSON响应。使用非
!版本的查询函数
如果「找不到用户」是业务流程的正常分支(比如用户登录时输入了无效ID),直接用不带!的get_admin_user/1,然后在action里显式处理分支:def show(conn, %{"id" => id}) do case AdminUsers.get_admin_user(id) do {:ok, admin_user} -> render(conn, "show.html", admin_user: admin_user) {:error, :not_found} -> render(conn, "404.html", message: "管理员不存在") end end自定义Plug统一处理异常
如果多个控制器都需要处理这类异常,可以写一个Plug来捕获Ecto.NoResultsError,统一返回响应:defmodule YourAppWeb.Plug.CatchNotFound do import Plug.Conn import Phoenix.Controller def init(opts), do: opts def call(conn, _opts) do try do conn rescue Ecto.NoResultsError -> conn |> put_status(:not_found) |> render(YourAppWeb.ErrorView, "404.html") |> halt() end end end之后在
router.ex里把这个Plug加到需要的管道中即可。灵活选择Ecto查询函数
除了get/3和get!/3,还可以用Repo.get_by/3(非!版本返回nil或结果)、Repo.one/1配合模式匹配,根据业务场景选最适合的查询方式,避免不必要的异常。
内容的提问来源于stack exchange,提问作者user3331142

