Is it possible to define a fallback template for ErrorHTML in Phoenix 1.7?

Is it possible to render a “fallback” component when an error doesn’t match one of the templates added to the error_html directory? The example given in Custom Error Pages — Phoenix v1.7.0 would just print the bare error string without rendering a component if a template is not found.

With Phoenix.View I could set up ErrorView like:

defmodule MyAppWeb.ErrorView do
  use MyAppWeb, :view

  def render("403.html", assigns) do
    render(
      "error.html",
      Map.merge(
        %{
          title: "Access denied",
          message: "Sorry, you do not have access to the requested page."
        },
        assigns
      )
    )
  end

  # another render() for 404.html etc...

  # and a fallback if a specific error is not handled above
  def template_not_found(template, assigns) do
    render(
      "error.html",
      Map.merge(
        %{
          title: Phoenix.Controller.status_message_from_template(template),
          message: "Sorry, we experienced an unexpected error."
        },
        assigns
      )
    )
  end
end

With the above I can have the 1 pretty error template that can handle all the errors, and pass in assigns with the error title and message.

I can’t seem to make this work with ErrorHTML. I either seem to get stuck in an inifinte render loop, or no matching clause.

1 Like

Yes, it is.

The basic flow of Phoenix is:

  1. Phoenix try to find the component whose name is a status code (let’s say 404 or 500), and render it.
  2. If the component is missing, Phoenix will try to call render/2.

You can inspect code at here.

Based on the basic flow, we can:

  1. define a fallback component - fallback/1.
  2. call fallback/1 in render/2.

Last, I’d like to share my code:

lib/hello_web/controllers/error_html.ex:

defmodule HelloWeb.ErrorHTML do
  use HelloWeb, :html

  embed_templates "error_html/*"

  # render the fallback template
  def render(template, assigns) do
    status = assigns.status
    message = Phoenix.Controller.status_message_from_template(template)
    fallback(%{status: status, message: message})
  end
end

lib/hello_web/controllers/error_html/fallback.html.heex:

<!DOCTYPE html>
<html lang="en" style="w-full h-full">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="csrf-token" content={get_csrf_token()} />
    <.live_title suffix=" · Hello">
      <%= assigns[:page_title] || "Home" %>
    </.live_title>
    <link rel="icon" href="/favicon.ico" />
    <link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
    <script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
    </script>
  </head>
  <body class={[
    "w-full min-h-screen",
    "flex flex-col justify-center items-center space-y-4",
    "antialiased"
  ]}>
    <h1 class="text-xl text-neutral-500 font-medium font-mono">
      <span><%= @status %></span>
      <span class="mx-2">|</span>
      <span class="uppercase"><%= @message %></span>
    </h1>
    <a class="flex items-center space-x-2 text-neutral-500 nav-link" href={~p"/"}>
      <Heroicons.arrow_left class="w-4 h-4" />
      <span>back to home page</span>
    </a>
  </body>
</html>
2 Likes

Oh, brilliant, I had no idea you could call a functional component like that… I assumed you had to call it like a component (in a heex block), but that works great. Thank you!

1 Like