It's been a couple of years that I don't build error pages in Elixir, and the last time I did there was no LiveView in the app. There are a few things that changed since then so here in this post we'll cover what we need to know to build those error pages.
First of all, the whole idea of the Error Page is to present to an user an ok page for when something not expected happens, such as a 404 - Not Found page. Ideally you should have no link that drives the user to a 404 error. Still, that happens. Even if your app is a bug-free app, which I doubt it, errors will happen eventually. The premise here is to create a simple page, with very minimal dependencies just to show a message to the user that we'll be working on this error. We want to guarantee that rendering the error page itself will not generate another error, no recursive errors :/
The work needed to implement a simple page is very minimal so there should be no excuse not to implement them, even if that happens only a few times per year. So let's start it!
Raising errors in dev mode
To start we need to change a config/dev.exs
to tell our endpoint that we want to show the real error page, not the Phoenix created error pages for debug. To do that apply this change:
# config/dev.exs
config :my_app, MyAppWeb.Endpoint,
#...
- debug_errors: true,
+ debug_errors: false,
#...
Now if you run the server and try to get a http://localhost:4000/some/wrong/path
that would show a blank page with the text Not Found
.
HTML templates
Phoenix uses a plug protocol called Plug.Exception
to infer what's going to be the HTTP response code per Elixir Exception that was raised. In case you want to create your own Custom Exceptions go ahead and do that, it's simple and it's nice.
On our case we'll just use the 404 erros from non mapped routes to test the HTML template. If you look for our Phoenix Endpoint config.exs
you'll see that we have a render_errors:
config that points to a module MyAppWeb.ErrorHTML
. Go to that module and add this line:
defmodule MyAPpWeb.ErrorHTML do
use MyAPpWeb, :html
+ embed_templates "error_html/*"
#...
end
Now create this file: lib/my_app_web/controllers/error_html/404.html.heex
:
<!-- lib/my_app_web/controllers/error_html/404.html.heex -->
<p>
Oops, The page you are looking for cannot be found.
</p>
<p>
Our team was notified and is working on that. Please go
<.link href={~p"/"}>
back to the site
</.link>
</p>
A couple caveats with the template
First of all, you do have access to the assigns
, so let's say that your error happened after you set your current_user
then you could use that in our error message if you want, just like any heex variable: <%= @current_user.email %>
, but remember that depends on the error that could have happened before that set_current_user call. And remember that we do want a zero chance of an error in the error page. On this example plug will check the routes before executing any plug. If they hit http://localhost:4000/some/wrong/path
then plug will halt that request before setting the current_user in the assigns.
Secondly you'll notice that none of your css and js will be available on those error pages. This was intentionally to minimize the chance of errors (again) and this was done by setting the root_layout
and the layout
as false. So if you want, let's say bring tailwind to the error pages so you can do some basic styling you will have to define a root_layout
for those error pages.
Adding a root layout
So go ahead and create a root layout for your errors on lib/my_app_web/controllers/error_html/root.html.heex
:
<!-- lib/my_app_web/controllers/error_html/root.html.heex -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="csrf-token" content={get_csrf_token()} />
<title>My App ยท Error</title>
<link phx-track-static rel="stylesheet" href={~p"/assets/tailwind.css"} />
</head>
<body>
<%= @inner_content %>
</body>
</html>
And let's change the config/config.exs
file:
config :my_app, MyAPpWeb.Endpoint,
url: [host: "localhost"],
render_errors: [
formats: [html: MyAPpWeb.ErrorHTML, json: MyAPpWeb.ErrorJSON],
+ root_layout: {MyAPpWeb.ErrorHTML, :root},
layout: false
],
Now we can use tailwind on each of those error pages.
Default to 500 error
For now we did not create any custom Exception mapping to http codes yet, and we are only mapping a 404 and a 500 errors. And I did a small change to render the template for the 500 error as the default by doing this change to the ErrorHTML
module:
defmodule MyAppWeb.ErrorHTML do
use MyAppWeb, :html
embed_templates "error_html/*"
def render(template, assigns) do
- Phoenix.Controller.status_message_from_template(template)
+ apply(__MODULE__, :"500", [assigns])
end
end
So in case a dev forget to create a specific error page for a new http code then we are covered here.
Just before commiting this don't forget to change the dev mode back to the default phoenix debug error pages as they are very useful:
# config/dev.exs
config :my_app, MyAppWeb.Endpoint,
#...
+ debug_errors: true,
- debug_errors: false,
#...
That's all that I have for today, thanks for reading!