Elegant Error Handling in Phoenix LiveView with Ash
Last updated on
- phoenix
- liveview
- ash
- error-handling
Elegant Error Handling in Phoenix LiveView with Ash
Effective error handling is crucial for web applications, directly affecting user experience and reliability. In this post, I'll show you how to implement clean and effective error handling for 404, 403, and 500 errors in a Phoenix LiveView application using Ash Framework.
The Problem
When using Phoenix and Ash Framework, you might encounter the frustrating error: no view was found for the format: 'html'
. This usually occurs when your app hits an error without a properly configured error handler.
Initially, I attempted a custom ErrorController
, only to realize Ash Framework already handles exceptions elegantly through the Plug Exception protocol. Understanding this made my solution significantly simpler and cleaner.
Configuring Elegant Error Handling
Phoenix offers built-in tools to streamline error handling. Here’s the step-by-step approach I followed to seamlessly integrate error handling into my Ash-powered LiveView app:
1. Configure the Endpoint
In your config/config.exs
, configure Phoenix to use custom templates:
config :my_app, MyAppWeb.Endpoint,
render_errors: [
formats: [html: MyAppWeb.ErrorHTML, json: MyAppWeb.ErrorJSON],
layout: {MyAppWeb.Layouts, :error}
]
This directs Phoenix to use MyAppWeb.ErrorHTML
and MyAppWeb.ErrorJSON
for rendering error pages.
2. Custom Error Layout
Create a dedicated error layout in lib/my_app_web/components/layouts/error.html.heex
:
<!DOCTYPE html>
<html lang=\"en\">
<head>
<meta charset=\"utf-8\" />
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />
<title>{Phoenix.Controller.status_message_from_template(\"#{@status}.html\")} · MyApp</title>
</head>
<body>
<main>
{@inner_content}
</main>
</body>
</html>
The key here is the dynamic title generation based on the error status code.
3. Custom Error Templates
Define custom templates clearly communicating each error:
404 Template (lib/my_app_web/controllers/error_html/404.html.heex
):
<section>
<h1>{@status}</h1>
<p>{Phoenix.Controller.status_message_from_template(\"#{@status}.html\")}</p>
<p>Sorry, the page you're looking for doesn't exist or has moved.</p>
<.link href=\"/\">Return to Home</.link>
</section>
500 Template:
<section>
<p>{Phoenix.Controller.status_message_from_template(\"#{@status}.html\")}</p>
<p>Something went wrong on our end. We're on it!</p>
</section>
403 Template:
<section>
<p>{Phoenix.Controller.status_message_from_template(\"#{@status}.html\")}</p>
<p>You don't have permission to access this resource.</p>
</section>
4. Error HTML Module
Use the standard Phoenix error HTML module, embedding your custom templates:
defmodule MyAppWeb.ErrorHTML do
use MyAppWeb, :html
embed_templates \"error_html/*\"
def render(template, _assigns) do
Phoenix.Controller.status_message_from_template(template)
end
end
Leveraging Ash's Bang Methods
Ash Framework integrates neatly by raising exceptions through its "bang" methods (like Ash.read!
). These exceptions trigger Phoenix’s built-in error handling automatically:
def mount(%{\"id\" => id}, _session, socket) do
post = MyApp.Content.get_post!(id, actor: socket.assigns.current_user)
{:ok, assign(socket, :post, post)}
end
If the resource doesn't exist or access is denied, Ash raises an exception that Phoenix gracefully catches, rendering the appropriate custom error template.
For more details, check Ash's Plug Exception implementation.
Practical Examples
Not Found (404):
def handle_event(\"view_post\", %{\"id\" => id}, socket) do
post = MyApp.Content.get_post!(id, actor: socket.assigns.current_user)
{:noreply, push_navigate(socket, to: ~p\"/posts/#{post.id}\")}
end
Forbidden (403):
def handle_event(\"comment_on_post\", params, socket) do
MyApp.Content.create_comment!(params, actor: socket.assigns.current_user)
{:noreply, put_flash(socket, :info, \"Comment added!\")}
end
Server Error (500):
def assign_posts(socket) do
try do
posts = MyApp.Content.list_posts!(actor: socket.assigns.current_user)
assign(socket, :posts, posts)
rescue
error ->
Logger.error(\"Error loading posts: #{inspect(error)}\")
raise error
end
end
Best Practices Learned
- Simple and clear messages: Inform users without technical jargon.
- Guidance: Always offer clear navigation or next steps.
- Visual Consistency: Make error pages feel part of your app.
- Logging: Log detailed errors server-side for debugging.
- Centralize logic: Define error handling once and reuse throughout.
Why This Approach Works
- Plug Exception Protocol: Ash errors translate seamlessly to HTTP status codes.
- Format-aware rendering: Phoenix automatically handles HTML and JSON requests correctly.
- Dynamic Templates: Easy customization based on status codes.
Key Benefits
- User-friendly pages that match your app's branding
- Accurate HTTP status codes for HTML and API responses
- Reduced complexity with built-in Phoenix mechanisms
- Cleaner, maintainable codebase
Conclusion
Robust error handling doesn't have to be complicated. By leveraging Phoenix’s built-in error handling and Ash's Plug Exception integration, you can create intuitive, attractive error pages with minimal effort.
Have you tackled custom error handling in your Phoenix app? I'd love to hear your approach!
Hope you enjoyed the read, if you have any questions, feel free to reach out to me on twitter @lifeiscontent