Subscribe to access all episodes. View plans →
Published August 5, 2018
Elixir 1.6
Phoenix 1.3
View code on GitHub
Here we have a simple application that lists a few different albums. We see we can view an album and edit an album. However, if we try to edit or view an album that doesn’t exist in the database, we’re directed back to our homepage along with a message.
Now let’s take a look at our controller that does this. We’ll open our album_controller.ex
and looking at our show
action we use our Records
context to get an album. And if one doesn’t exist, we’re returning an error tuple with a message, and then redirecting to the album’s “index” action.
Now if we look at our other actions, we notice a similar pattern. If we return an OK tuple we proceed as expected. If there’s an issue, we return an error tuple and redirect to the homepage with a message. Let’s see how we can simplify these controller actions by only worrying about our happy path in the controller.
To do this we’ll use a great feature - action_fallback
- that was introduced in Phoenix 1.3. action_fallback
allows us to specify a Plug to handle cases where our controller action doesn’t return a connection. This can simplify our Phoenix controllers and make them much easier to maintain.
So let’s update our album_controller.ex
to use it. First let’s rewrite our show
action to only care about the “success” case. We’ll rewrite it to use a with
statement.
lib/teacher_web/controllers/album_controller.ex
...
def show(conn, %{"id" => id}) do
with {:ok, album} <- Records.get_album(id) do
render(conn, "show.html", album: album)
end
end
...
Now if we aren’t able to lookup an album this wont return a connection.
Let’s check that this still works. We’ll open our browser - and go to an album that’s listed, it works.
Now if we go to an album that doesn’t exist - we get an error. Great, now let’s add our fallback.
We’ll go back to the album_controller.ex
And let’s set our action_fallback
as a FallbackController
.
lib/teacher_web/controllers/album_controller.ex
...
action_fallback TeacherWeb.FallbackController
...
Now let’s create our FallbackController
. We’ll create a new file named fallback_controller.ex
. The we’ll define our module. And to handle our fallback we’ll simply create a function named call
that takes the connection, and then arguments - the arguments will be our error.
So in this case our arguments will be an error tuple where the second element is our message. Let’s go ahead and pattern match on it. And we’ll add a guard clause for when our msg
is a binary. Then in the function, we’ll just use the same logic for our error case that we had in our show
action. We’ll take our connection, add a flash message, and then redirect to the album’s index page.
lib/teacher_web/controllers/fallback_controller.ex
defmodule TeacherWeb.FallbackController do
use TeacherWeb, :controller
def call(conn, {:error, msg}) when is_binary(msg) do
conn
|> put_flash(:error, msg)
|> redirect(to: album_path(conn, :index))
end
end
Now let’s test it out.
If we go back to the browser and refresh the page for an album that doesn’t exist - perfect, we’re taken back to the homepage and our message is displayed.
Now let’s go back to our album_controller.ex
and remove our commented-out code.
Then let’s update our edit
action. Again, we’ll use a with
statement.
Now our show
and edit
actions are only worrying about our success case. And because they both return the same kind of error tuple - with a message as the second element - and have the same error logic, our single call
function will work for them.
We can already see how much simpler our actions are now that we aren’t worried about errors.
Now let’s update two of our other actions: update
and create
. Let’s start with update
and in instances where it’s a nested case
statement, like we have here, with
is a great option to help us clean up our code.
So we’ll get our album, then if we’re able to look it up we’ll update the album. Then we’ll render a flash message and redirect to the album’s page.
With our action updated, let’s remove the old code.
And now we can update the create
action. We’ll use another with
statement for our Records.create_album
function. Then add our flash and redirect when an album is created successfully.
lib/teacher_web/controllers/album_controller.ex
...
def edit(conn, %{"id" => id}) do
with {:ok, album} <- Records.get_album(id) do
changeset = Records.change_album(album)
render(conn, "edit.html", album: album, changeset: changeset)
end
end
def update(conn, %{"id" => id, "album" => album_params}) do
with {:ok, album} <- Records.get_album(id),
{:ok, album} <- Records.update_album(album, album_params) do
conn
|> put_flash(:info, "Album updated successfully")
|> redirect(to: album_path(conn, :show, album))
end
end
def create(conn, %{"album" => album_params}) do
with {:ok, album} <- Records.create_album(album_params) do
conn
|> put_flash(:info, "Album created successfully.")
|> redirect(to: album_path(conn, :show, album))
end
end
...
With these updated, we’ll now need to add another call
function that can act as our fallback for these actions when it returns an error tuple where the second element is an Ecto.Changeset
.
Let’s copy our logic for an error and remove the old code.
We’ll go to our fallback_controller.ex
and define another call
function that will take our connection, then we’ll pattern match on our error tuple with an Ecto.changeset
.
Inside the function let’s paste in our redirect from the create
action. Now this function will match when there’s an error for either our create
or update
actions. However, looking at the flash message here, it’s specific to the create
action.
Let’s create a way to display a flash message for either our update or create actions. We’ll create a new private function called get_msg
that will take our connection. Then we’ll use the action_name
function to get the name of the controller action.
With that let’s create another private function that will hold our different messages. Lets call it error_msgs
and we’ll create one for our create
action and another for our update
. Then we’ll call Map.get
passing in our error messages and then the action we want to get the error for. Let’s also give it a general default message to return.
Great now we can update our call
function to use our new get_msg
function to get the correct flash message to display,
lib/teacher_web/controllers/fallback_controller.ex
...
def call(conn, {:error, %Ecto.Changeset{}}) do
conn
|> put_flash(:error, get_msg(conn))
|> redirect(to: album_path(conn, :index))
end
defp get_msg(conn) do
name = action_name(conn)
Map.get(error_msgs(), name, "There was an issue")
end
defp error_msgs do
%{create: "There was an issue creating the movie",
update: "There was an issue updating the movie"}
end
...
Now we can go to the browser and test our changes out. And if we try to create a new album with no information it should fail and take us to the homepage with the correct message.
Perfect we’re redirected and our message is displayed. action_fallback
provides a great way to simplify your Phoenix controllers.
Kiere El-Shafie
6 years agoThe only concern I have with this episode is that the create/update example shown forfeits the user experience provided by a failed Changeset. The re-render of the new/edit template (form) showing the user exactly what errors they made is a better experience.
I understand this episode is just illustrating a point, but perhaps there should have been a comment that “you probably don’t want to do this for your create/update actions” just so beginners to Phoenix don’t think this is the proper way to build your create/update actions.
Alekx
6 years agoHey Kiere, thanks for the feedback. I agree, there should be a comment explaining that. I’ll update this video to include that.