Tom Hughes

Handling Exceptions in Phoenix Applications

25th April, 2020

Working with Elixir and Phoenix is a great experience with one of the key reasons being that code is explicit, without much 'magic'. However, one thing I struggled to understand was how a valid HTTP response would be sent with the correct status code when errors are not explicitly handled in controllers.

Take a look at this code generated by the mix phx.gen json command.

def show(conn, %{"id" => id}) do
  post = Posts.get_post!(id)
  render(conn, "show.json", post: post)
end
@doc """
Gets a single post.

Raises `Ecto.NoResultsError` if the Post does not exist.

## Examples

    iex> get_post!(123)
    %Post{}

    iex> get_post!(456)
    ** (Ecto.NoResultsError)

"""
def get_post!(id), do: Repo.get!(Post, id)

Our controller calls Posts.get_post!/1, when a function has an exclamation mark in its name we know it raises an exception, but nowhere in our controller is the exception handled.

If we setup our project to allow the Posts controller to be called:

scope "/api", ExampleWeb do
  pipe_through(:api)
  resources("/posts", PostController)
end

As well as disable development error messages:

debug_errors: false

If we then make a HTTP request to the /api/posts/1 endpoint, with no data in the database we receive a 404 response with the following data:

Doing so returns the following JSON with a 404 response status.

{"errors":{"detail":"Not Found"}}

Despite our controller not having any explicit code that states a 404 should be returned Phoenix handles the error appropriately.

Background Knowledge

To answer how Phoenix knows the response code to return we must first know a little about the Plug library, which Phoenix uses makes heavy use of as part of its HTTP layer.

The component we should know about is the Plug.Exception protocol. This protocol extends exceptions to make them aware of HTTP status codes.

Lets review part of the comment in the context example above. It says that get_post!/1 will raise a Ecto.NoResultsError.

Raises `Ecto.NoResultsError` if the Post does not exist.

If we look inside the phoenix_ecto dependency and navigate to the plug.ex file you will see how the error codes are mapped to the specific error from Ecto.

To discover how Phoenix sends the JSON response we must look into our configuration, inside the configuration we will see the render_errors key.

render_errors: [view: ExampleWeb.ErrorView, accepts: ~w(json)],

This states that we should render the ErrorView template whenever an error occurs in the application. The default Phoenix endpoint then takes care of this.

Implementing a Custom ERror

Now that we know that Phoenix handles these errors automatically for us, lets implement Plug.Exception for a custom error of our own. For this error we are going to return a 418 I'm a teapot status code.

Lets define a custom exception named TeaPotError.

defmodule TeaPotError do
  defexception message: "This is our custom TeaPotError!"
end

I'm now going to raise this error in our context, where we would normally request a Post from the repo.

def get_post!(id), do: raise(TeaPotError)

If we now send a request to /api/posts/1 we are now getting an error, but it's not quite the right error, instead we're getting a 500 error with the following response.

{"errors":{"detail":"Internal Server Error"}}

It looks like Phoenix doesn't know how to handle this error, if you look in your developer console you will see Phoenix has logged a stack trace for an error. Not to worry, armed with our existing knowledge we now know we can attach a status code to an exception with Plug.Exception.

defimpl Plug.Exception, for: TeaPotError do
  def status(_exception), do: 418
end

If we make that same request again we can now see that we get a 418 back, and our implementation worked.

{"errors":{"detail":"I'm a teapot"}}

You may notice here that the message returned "I'm a teapot" is not a message that we set. This message comes from the error_view.ex file, lets take a look.

defmodule ExampleWeb.ErrorView do
  use ExampleWeb, :view

  # If you want to customize a particular status code
  # for a certain format, you may uncomment below.
  # def render("500.json", _assigns) do
  #   %{errors: %{detail: "Internal Server Error"}}
  # end

  # By default, Phoenix returns the status message from
  # the template name. For example, "404.json" becomes
  # "Not Found".
  def template_not_found(template, _assigns) do
    %{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}}
  end
end

The Phoenix.Controller.status_message_from_template/1 function is being called instead of our custom message.

Therefore if we want to change the message returned for a 418 error we can add a method for ErrorView to match.

def render("418.json", _assigns) do
  %{errors: %{detail: "I can't brew coffee, because I am a teapot!"}}
end

Now when we hit our endpoint we see our custom message returned. If we want to use the message we defined in the exception we can do so.

  def render("418.json", %{conn: conn}) do
    %{errors: %{detail: conn.assigns.reason.message}}
  end

I hope this has helped you understand error handling in Phoenix, don't hesitate to contact me for any questions or corrections.