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
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:
- you have two simple functions, a happy path function and a sad path function, instead of one function handling both.
- 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.
- 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
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
No to me you are already following the recommendations, the return codes are specific and not overlapping.
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
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.







