skills/fearovex/claude-config/elixir-antipatterns

elixir-antipatterns

Installation
SKILL.md

When to Use

Triggers: During code review, refactoring, or when writing Phoenix/Ecto code.

Load this skill when:

  • Writing Elixir modules and functions
  • Working with Phoenix Framework (Controllers, LiveView)
  • Building Ecto schemas and database queries
  • Implementing BEAM concurrency (Task, GenServer)
  • Handling errors with tagged tuples
  • Writing tests with ExUnit

Anti-patterns

Quick reference to the 8 core patterns:

  1. Tagged Tuples: Return {:ok, value} | {:error, reason} instead of nil or exceptions
  2. Explicit @spec: Document error cases in function signatures
  3. Context Separation: Business logic in contexts, not LiveView
  4. Preload Associations: Use Repo.preload/2 to avoid N+1 queries
  5. with Arrow Binding: Use <- for all failable operations in with
  6. Database Indexes: Index frequently queried columns
  7. Test Assertions: Every test must assert expected behavior
  8. Cohesive Functions: Group with chains >4 steps into functions

Code Examples

Example 1: Error Handling with Tagged Tuples

# ✅ CORRECT - Errors as values, explicit in @spec
defmodule UserService do
  @spec fetch_user(String.t()) :: {:ok, User.t()} | {:error, :not_found}
  def fetch_user(id) do
    case Repo.get(User, id) do
      nil -> {:error, :not_found}
      user -> {:ok, user}
    end
  end
end

# ❌ BAD - Exceptions for business errors
def fetch_user(id) do
  Repo.get(User, id) || raise "User not found"
end

Example 2: Phoenix LiveView with Context Separation

# ✅ CORRECT - Thin LiveView, logic in context
defmodule MyAppWeb.UserLive.Index do
  use MyAppWeb, :live_view

  def handle_event("create", params, socket) do
    case Accounts.create_user(params) do
      {:ok, user} -> {:noreply, redirect(socket, to: ~p"/users/#{user}")}
      {:error, changeset} -> {:noreply, assign(socket, changeset: changeset)}
    end
  end
end

# ❌ BAD - Business logic in LiveView
def handle_event("create", %{"user" => params}, socket) do
  if String.length(params["name"]) < 3 do
    {:noreply, put_flash(socket, :error, "Too short")}
  else
    case Repo.insert(User.changeset(%User{}, params)) do
      {:ok, user} -> send_email(user); redirect(socket)
    end
  end
end

Example 3: Ecto N+1 Query Optimization

# ✅ CORRECT - Preload associations (2 queries total)
users = User |> Repo.all() |> Repo.preload(:posts)
Enum.map(users, fn user -> process(user, user.posts) end)

# ❌ BAD - Query in loop (101 queries for 100 users)
users = Repo.all(User)
Enum.map(users, fn user ->
  posts = Repo.all(from p in Post, where: p.user_id == ^user.id)
  {user, posts}
end)

Anti-Patterns

Error Management

Don't: Use raise for Business Errors

# ❌ BAD
def fetch_user(id) do
  Repo.get(User, id) || raise "User not found"
end

# ✅ CORRECT
@spec fetch_user(String.t()) :: {:ok, User.t()} | {:error, :not_found}
def fetch_user(id) do
  case Repo.get(User, id) do
    nil -> {:error, :not_found}
    user -> {:ok, user}
  end
end

Don't: Return nil for Errors

# ❌ BAD
def find_user(email), do: Repo.get_by(User, email: email)

# ✅ CORRECT
@spec find_user(String.t()) :: {:ok, User.t()} | {:error, :not_found}
def find_user(email) do
  case Repo.get_by(User, email: email) do
    nil -> {:error, :not_found}
    user -> {:ok, user}
  end
end

Don't: Use = Inside with for Failable Operations

# ❌ BAD
with {:ok, user} <- fetch_user(id),
     validated = validate(user),  # ← Doesn't check for {:error, _}
     {:ok, saved} <- save(validated) do
  {:ok, saved}
end

# ✅ CORRECT
with {:ok, user} <- fetch_user(id),
     {:ok, validated} <- validate(user),
     {:ok, saved} <- save(validated) do
  {:ok, saved}
end

Architecture & Boundaries

Don't: Put Business Logic in LiveView

# ❌ BAD - Validation in view
def handle_event("create", %{"user" => params}, socket) do
  if String.length(params["name"]) < 3 do
    {:noreply, put_flash(socket, :error, "Too short")}
  else
    Repo.insert(User.changeset(%User{}, params))
  end
end

# ✅ CORRECT - Delegate to context
def handle_event("create", params, socket) do
  case Accounts.create_user(params) do
    {:ok, user} -> {:noreply, redirect(socket, to: ~p"/users/#{user}")}
    {:error, changeset} -> {:noreply, assign(socket, changeset: changeset)}
  end
end

Don't: Chain More Than 4 Steps in with

# ❌ BAD - Too many responsibilities
with {:ok, a} <- step1(),
     {:ok, b} <- step2(a),
     {:ok, c} <- step3(b),
     {:ok, d} <- step4(c),
     {:ok, e} <- step5(d) do
  {:ok, e}
end

# ✅ CORRECT - Group into cohesive functions
with {:ok, validated} <- validate_and_fetch(id),
     {:ok, processed} <- process_business_rules(validated),
     {:ok, result} <- persist_and_notify(processed) do
  {:ok, result}
end

Data & Performance

Don't: Query Without Indexes

# ❌ BAD
create table(:users) do
  add :email, :string
end

# ✅ CORRECT
create table(:users) do
  add :email, :string
end
create unique_index(:users, [:email])

Testing

Don't: Write Tests Without Assertions

# ❌ BAD
test "creates user" do
  UserService.create_user(%{name: "Juan"})
end

# ✅ CORRECT
test "creates user successfully" do
  assert {:ok, user} = UserService.create_user(%{name: "Juan"})
  assert user.name == "Juan"
end

Quick Reference

Situation Anti-Pattern Correct Pattern
Error handling raise "Not found" {:error, :not_found}
Missing data Return nil {:error, :not_found}
Business logic In LiveView In context modules
Associations Enum.map + Repo.get Repo.preload
with chains validated = fn() {:ok, validated} <- fn()
Frequent queries No index create index(:table, [:column])
Testing No assertions assert expected behavior
Complex logic 6+ step with Group into 3 functions

Rules

  • This skill is a catalog of what NOT to do — every pattern here represents a code smell or anti-pattern to avoid or eliminate during review
  • Apply this skill during code review and refactoring sessions, not as a guide for initial implementation
  • Ecto queries must not be constructed inside business logic modules — keep query logic in dedicated query modules or Ecto schemas
  • Error handling in Phoenix controllers must use pattern matching on tagged tuples ({:ok, _} / {:error, _}), not bare exceptions
  • Testing anti-patterns (e.g., testing implementation details instead of behavior) are as critical to fix as runtime anti-patterns
Weekly Installs
3
First Seen
Mar 22, 2026