What's the best way of handling error?

Hello.

I’m a beginner programmer from Japan.

I’m in trouble of handling error…

auth_controller.ex

def signup(conn, params) do
    url = @db_domain_url <> @api_url <> @signup_url
    try do
      attrs = Poison.encode!(params)
    rescue
      _ -> 
        render("error.json", "Failed to encode json from client", 2000)
    end

    try do
      {:ok, response} = HTTPoison.post(url, attrs, @content_type)
    rescue
      _ -> 
        render("error.json", "POST Request failed", 3000)
    end

    try do
      body = Poison.decode!(response.body)
    rescue
      _ ->
        render("error.json", "Failed to decode json from DB server", 2001)
    end

    render("show.json", body)
  end

auth_view.ex

defmodule PappapWeb.AuthView do
  use PappapWeb, :view

  def render("show.json", %{} = body) do
    body
  end

  def render("error.json", msg, error_no) do
    %{
      "result" => false,
      "reason" => msg,
      "error_no" => error_no
    }
  end
end

Those are codes in my Request-Handling server. It receives requests from ios, and then it fetches data from Database server.
To prevent crash of ios, I want to handle error in good ways. But I don’t know how to do that. With those codes, my server did not start due to an error.

== Compilation error in file lib/pappap_web/controllers/auth_controller.ex ==
** (CompileError) lib/pappap_web/controllers/auth_controller.ex:20: undefined function attrs/0
    (elixir 1.10.3) src/elixir_locals.erl:114: anonymous fn/3 in :elixir_locals.ensure_no_undefined_local/3
    (stdlib 3.13) erl_eval.erl:680: :erl_eval.do_apply/6
    (elixir 1.10.3) lib/kernel/parallel_compiler.ex:304: anonymous fn/4 in Kernel.ParallelCompiler.spawn_workers/7

I know it is a problem of variable scopes… but I’m not sure what is the best way to handle errors…

Help me!

with {:req, {:ok, attrs}} <- {:req, Poison.encode(params)},
     {:http, {:ok, response}} <- {:http, HTTPoison.post(url, attrs, @content_type)},
     {:resp, {:ok, body}} <- {:resp, Poison.decode(response.body)}
do
  render("show.json", body)
else
  {:req, _} -> render("error.json", "Failed to encode json from client", 2000)
  {:http, _} -> render("error.json", "POST Request failed", 3000)
  {:resp, _} -> render("error.json", "Failed to decode json from DB server", 2001)
end

Alternatively you can use action_fallback/1 for handling errors:

# Handler
with {:req, {:ok, attrs}} <- {:req, Poison.encode(params)},
     {:http, {:ok, response}} <- {:http, HTTPoison.post(url, attrs, @content_type)},
     {:resp, {:ok, body}} <- {:resp, Poison.decode(response.body)}
do
  render("show.json", body)
end

# Action fallback

def call(conn, {:req, _}), do: render("error.json", "Failed to encode json from client", 2000)
def call(conn, {:http, _}), do: render("error.json", "POST Request failed", 3000)
def call(conn, {:resp, _}), do: render("error.json", "Failed to decode json from DB server", 2001)
9 Likes

It is not a common pattern to use try and catch clause… and Yes, it’s a scoping problem.

But before solving the scope, You might want to refactor a little bit.

If You think something can go wrong, You could use the “soft” Poison.encode, instead of Poison.encode!, which raise an error.

You just have to deal with {:ok, result}, or {:error, reason}.

And because You have multiple conditions, I would be tempted to use with.

with {:ok, attrs} <- Poison.encode(params),
  {:ok, response} <- HTTPoison.post(url, attrs, @content_type),
  {:ok, body} <- Poison.decode(response.body) do
  render("show.json", body)
else
  ... # Manage errors here!
end

I would always try to avoid try and catch

5 Likes

Thank you for telling me the solutions!

Sorry, I don’t really understand what is the common way… And why do you always try to avoid try and catch? Please teach me why :bowing_man:

Because exceptions are mean to be used in exceptional cases, not when you need flow control. All of the cases mentioned above are expected cases, not exceptions.

6 Likes

Thanks, I see! :laughing:
I continue trying to write a better code than before :muscle:

5 Likes

I got another issue, I’m writing a program around Ecto.

def create_user(attrs \\ %{}) do
    try do
      %User{}
      |> User.signup_changeset(attrs)
      |> Repo.insert()
    rescue
      e in Ecto.ConstraintError -> {:error, to_string(e.message), 1001}
      _ -> {:error, "Unexpected error in schema", 2000}
    end
  end

This is my function for creating user and insert it into postgres, and my auth_controller.ex calls the function.

def signup(conn, params) do
    params = Map.merge(params, %{"create_time" => Date.utc_today})

    with {:ok, user} <- User.create_user(params) do
      render(conn, "user.json", user: user)
    else
      {:error, reason, error_no} ->
        map = %{
          "result" => false,
          "reason" => reason,
          "error_no" => error_no
        }
        json(conn, map)

      _ ->
        map = %{
          "result" => false,
          "reason" => "Unexpected error",
          "error_no" => 10000
        }
        json(conn, map)
    end
  end

Calling the controller, it responds like this when create_user throws a constraint error.

{
    "error_no": 1001,
    "reason": "constraint error when attempting to insert struct:\n\n    * user_email_index (unique_constraint)\n\nIf you would like to stop this constraint violation from raising an\nexception and instead add it as an error to your changeset, please\ncall `unique_constraint/3` on your changeset with the constraint\n`:name` as an option.\n\nThe changeset has not defined any constraint.\n",
    "result": false
}

That I want to ask you is, “Is my error handling not bad?” I just want to know a better way to handle the error.
Thanks.

Instead of rescuing on constraint error use check_constraint/{2,3} or unique_constraint/{2,3}.

In addition to that, returning HTTP 200 OK on error with indicating error only in body is in general bad practice.

By the way, if you are using timestamps/0 in your schema, then you can use inserted_at column as your create_time if that is what you expect.

3 Likes

I suppose You are using an old Phoenix API, because You use Poison, and no contexts

Again try and catch is not the pattern… it should be {:ok, user} | {:error, changeset}

And then, You could use fallback controllers to simplify your code, and use dedicated helper for rendering json error from changeset.

To render error changeset, You could use code like this…

  def render("error.json", %{changeset: changeset}) do
    %{
      status: "failure",
      errors: Ecto.Changeset.traverse_errors(changeset, &translate_error/1)
    }
  end

If You feel the need to use try and catch, please refrain :slight_smile:

4 Likes

@hauleth @kokolegorille
Thank you for your answers!

I tried to recreate my project with contexts, and deleted create_time column.

Sorry, I don’t understand what you mean due to short of knowledge…
You mean like error information must be contained in head as well? Please teach me that :bowing_man:

I found the render function in the new project! Thanks :smile:

Can action_fallback be used with Liveview? All of the examples I see pass in a conn and not a socket.

No, it is done for controllers, in particular, JSON controllers.

You need to use another mecanism for liveview.

All of the examples for liveview use case or with statements, which is what I’ve been doing. Is there anything more efficient? action_fallback looked really attractive since it moves all of the error handling out.

It would be difficult to abstract error handling in liveview, because the source of error is not generic…

is it an error in handle_event, or handle_info, or in a component?!

Anyway, You could probably do your own liveview error handling module, that could be called from any liveview.

What I’m trying to get away from is all of these embedded case statements like this (I’m abbreviating but this is the general setup:

defp create_event(params) do
  case Events.create_event(params) do
    {:ok, event} ->
        case Reminders.create_reminder(event) do
          {:ok, reminder} -> do something
          {:error, changeset} -> flash error
    {:error, changeset} -> flash error
  end

What I wish I could do is this:

defp create_event(params) do
  with {:ok, event} <- create_event(params)
          {:ok, reminder} <- create_reminder(event)
  do something with event and reminder
end

I know I could add an “else” statement to that and handle {:error, changeset} but I was trying to see if there is a more elegant way of dealing with errors.