Displaying Changeset error strings in Phoenix

Sometimes, when writing APIs, it becomes necessary to produce readable validation errors to clients. Often times, the default implementations of servers render JSON objects that require client-side formatting to present readable error messages to the user.

Take this common Rails practice, for example:

<div>  
  <% if @object.errors.full_messages.any? %>
  <ul>
    <% @object.errors.full_messages.each do |message| %>
      <li>
        <%= message %>
      </li>
    <% end %>    
  </ul>
  <% end %>
</div>  

This type of template is usually put into a partial of some sort, which is then called in every view. Yuck!

In Phoenix, validation errors in Ecto changesets are usually in the following keyword list structure:

changeset = Post.changeset(%Post{}, %{})  
changeset.valid? # false  
changeset.errors # [title: "can't be blank"]  

Which gets rendered into JSON:

{errors: {title: "can't be blank"}}

On mobile clients in particular, it may be undesirable to convert this JSON into plain readable strings on the device itself. Consider then an alternative method of displaying errors. Phoenix generates two error-related files under web/views: error_helpers.ex and error_view.ex. As a helper method:

# error_helpers.ex

defmodule MyApp.ErrorHelpers do  
  def error_string_from_changeset(changeset) do
    Enum.map(changeset.errors, fn {k, v} ->
       "#{Phoenix.Naming.humanize(k)} #{translate_error(v)}"
    end) |> Enum.join(". ")
  end
end

# error_view.ex
defmodule MyApp.ErrorView do  
  @doc """
  Renders an error from a changeset. `reason` can be either a string
  or a changeset.
  """
  def render("error.json", %{reason: reason}) do
    reason = cond do
      is_nil(reason) || reason == "" -> "Unknown error."
      is_bitstring(reason) -> reason
      true -> error_string_from_changeset(reason)
    end

    %{error: reason}
  end
end  

With just a bit of extra logic in the error renderer, it becomes much easier to send error messages back to clients—and easier to display them. On iOS, for example, you can use a library like SwiftMessages in combination with your own helper function:

import SwiftMessages

struct FlashHelper {  
    static func displayMessageWithTitle(title: String, body: String, theme: Theme) {
        let message = MessageView.viewFromNib(layout: .CardView)
        message.configureTheme(theme)
        message.configureContent(title: title, body: body)
        message.button?.hidden = true
        SwiftMessages.show(view: message)
    }

    static func displayError(error: NSError) {
        displayMessageWithTitle("Error", body: error.localizedDescription, theme: .Error)
    }
}

Error handling with network calls then becomes especially clean. With this, passing around common error handlers into Alamofire should clean up code substantially.