coen.bakker

coen.bakker

Is this a violation of the anti-pattern Complex Else Clauses in With"?

Is book/2 in violation of the anti-pattern called Complex Else Clauses in With?

I reckoned it is not because the else clauses use the conn variable. I could pass those to the private functions parse_id/1 and fetch_book/1, but not sure if that is recommended…

# Part of an elixirland.dev exercise
defmodule BookClubWeb.APIController do
  use BookClubWeb, :controller
  alias BookClub.Books

  def books(conn, %{"title" => title}) do
    books = Books.list_books_with_active_or_first_page(filter: title)
    render_pretty_json(conn, books)
  end

  def books(conn, _params) do
    books = Books.list_books_with_active_or_first_page()
    render_pretty_json(conn, books)
  end

  def book(conn, %{"id" => id}) do
    with {:ok, id} <- parse_id(id),
         {:ok, book} <- fetch_book(id) do
      render_pretty_json(conn, book)
    else
      {:error, :invalid_id} ->
        conn
        |> put_status(:bad_request)
        |> json(%{error: "Invalid ID"})

      {:error, :not_found} ->
        conn
        |> put_status(:not_found)
        |> json(%{error: "Book not found"})
    end
  end

  defp render_pretty_json(conn, data) do
    json_string = Jason.encode!(data, pretty: true)
    conn
    |> put_resp_content_type("application/json")
    |> send_resp(200, json_string)
  end

  defp parse_id(id) do
    case Integer.parse(id) do
      {id, _} when id >= 1 -> {:ok, id}
      _ -> {:error, :invalid_id}
    end
  end

  defp fetch_book(id) do
    case Books.get_book_with_active_or_first_page(id) do
      nil -> {:error, :not_found}
      book -> {:ok, book}
    end
  end
end

Most Liked

zachdaniel

zachdaniel

Creator of Ash

I would actually prefer something like this:

  def book(conn, %{"id" => id}) do
    with {:ok, id} <- parse_id(id),
         {:ok, book} <- fetch_book(id) do
      render_pretty_json(conn, book)
    else
      {:error, error} ->
        handle_error(conn, error)
    end
  end

  defp handle_error(conn, :bad_request) do
    conn
     |> put_status(:bad_request)
     |> json(%{error: "Invalid ID"})
  end
        
  defp handle_error(conn, :not_found) do
    conn
    |> put_status(:not_found)
    |> json(%{error: "Book not found"})
  end

My reasoning:

  1. you have two simple functions, a happy path function and a sad path function, instead of one function handling both.
  2. I wouldn’t want the functions returning errors to tell me what their status is. Their job is to do the thing or tell me something went wrong, not to provide an HTTP status. Thats the controller action’s job.
  3. it lets the programmer looking at the code not think about errors if they aren’t what they are here for, but they can see where errors are handled.
joey_the_snake

joey_the_snake

Sorry on phone so can’t type out code, but I think in your case I would take the conn stuff out of the parse and fetch functions. I would have the book function return either ok or error tuple and then do the conn stuff on the return from that function. That way you are talking away the need to ever care about which error comes from which with clause. You just know the book function can return different kinds of errors

Hermanverschooten

Hermanverschooten

No to me you are already following the recommendations, the return codes are specific and not overlapping.

tfwright

tfwright

I think this is right, and this kind of a thing is a challenge with a lot of anti-patterns. Lots of clauses adds complexity, but so do functions, so it’s not the case that simply replacing one with the other will always result in simpler code that all experienced developers agree is better/more maintainable. I know Jose has been trying to pare down that list to just the most important and objective pain points, but I almost feel like the top of that doc needs a big YMMV so people new to Elixir don’t take it too seriously and worse, try to use it as a club against other devs.

D4no0

D4no0

What I usually do is use fallback controller using action_fallback/1 for this:

defmodule MyFallbackController do
  use Phoenix.Controller

  def call(conn, {:error, :not_found}) do
    conn
    |> put_status(:not_found)
    |> put_view(MyErrorView)
    |> render(:"404")
  end

  def call(conn, {:error, :unauthorized}) do
    conn
    |> put_status(:forbidden)
    |> put_view(MyErrorView)
    |> render(:"403")
  end
end

In this way you have a single place where you handle all the potential errors and you can use with in your codebase without any concerns on intermediate error handling. As long as your return value != conn from your plugs, the fallback action will be always triggered.

Where Next?

Popular in Questions Top

Werner
Hi, I’m using Ubuntu 18.04 and after updating to OTP-24.0 yesterday i have this warning when I run “mix local.hex”: 14:57:30.512 [warn] ...
New
logicmason
Hi there, I'm working through my first release with elixir/phoenix. I've built a release with distillery and found that it crashes when I...
New
lk-geimfari
What is most correct way to open, read and parse JSON file with poison? For example if we have example.json file in root of some projec...
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
Phillipp
Hey, I have a NanoPi-M3 and try to install Elixir on their Ubuntu image. I followed the Raspberry Pi installation instructions from the ...
New
chensan
I have a User schema with a :from_id field set to type :string: defmodule TweetBot.Repo.Migrations.CreateUsers do use Ecto.Migration ...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
Exadra37
Sometimes I want to check if the input into a function is not a blank string. My first approach: defmodule Example do def do_stuff(s...
New
wernerlaude
In AR this is so simple @articles = current_user.articles How to do in Ecto? def index(conn, _params) do current_user = conn.assig...
New
jc00ke
Expanding on this topic: https://forum.elixirforum.net/t/map-typespec-question/19217 Let’s say I have a map with required and optional k...
New

Other popular topics Top

srinivasu
How to handle excepions in elixir? Suppose i have A, B, C ,D, E modules. and each module has get() function. A.get() method will call th...
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
AstonJ
You’re a programmer, so you don’t need spoon feeding with the conventional drivel about “this is an integer.” No. You need to know what’s...
New
KronicDeth
Elixir plugin for JetBrain’s IntelliJ Platform (including Rubymine) This is a plugin that adds support for Elixir to JetBrains IntelliJ...
289 35421 110
New
vac
Hi, I'm quite new in Elixir and I'm trying to format a string to a PEM format. I have the certificate value like MIIDBTCCAe2...... and ...
New
rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list....
New
chrisalley
ExUnit now has describe blocks which is a welcome addition coming from RSpec. In the docs, it states that nested hierarchies of describe ...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID&lt;0.412.0&gt; terminating ** (Postgrex.Error) FATAL...
New
josevalim
Hi everyone, One of the features added to Elixir early on to help integration with Erlang code was the idea of overridable function defi...
New
Nvim
Elixir appears to be a superior language to Python. I don’t see any advantage of Python over Elixir. Are there any?
New

We're in Beta

About us Mission Statement