Check out the Alchemist's Edition

#62: Simplifying Phoenix Controllers with action_fallback

Elixir 1.6

Phoenix 1.3

View code on GitHub


Sign up for our newsletter to get notified when new episodes drop

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/album_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.