elixir-antipatterns
Original:🇺🇸 English
Translated
Core catalog of 8 critical Elixir/Phoenix anti-patterns covering error handling, separation of concerns, Ecto queries, and testing. Trigger: During Elixir code review, refactoring sessions, or when writing Phoenix/Ecto code.
1installs
Added on
NPX Install
npx skill4agent add gentleman-programming/gentleman-skills elixir-antipatternsTags
Translated version includes tags in frontmatterSKILL.md Content
View Translation Comparison →Elixir Anti-Patterns
Critical anti-patterns that compromise robustness and maintainability in Elixir/Phoenix applications.
Complement with:andmix formatfor style enforcementCredo
Extended reference: Seefor 40+ patterns and deep-dive examplesEXTENDED.md
When to Use
Topics: Error handling (3 patterns) • Architecture (2 patterns) • Performance (2 patterns) • Testing (1 pattern)
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
Critical Patterns
Quick reference to the 8 core patterns this skill enforces:
- Tagged Tuples: Return instead of
{:ok, value} | {:error, reason}or exceptionsnil - Explicit @spec: Document error cases in function signatures
- Context Separation: Business logic in contexts, not LiveView
- Preload Associations: Use to avoid N+1 queries
Repo.preload/2 - with Arrow Binding: Use for all failable operations in
<-with - Database Indexes: Index frequently queried columns
- Test Assertions: Every test must assert expected behavior
- Cohesive Functions: Group chains >4 steps into functions
with
Seesection below for detailed ❌ BAD / ✅ CORRECT code examples.## Anti-Patterns
Code Examples
Example 1: Error Handling with Tagged Tuples
elixir
# ✅ 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"
endExample 2: Phoenix LiveView with Context Separation
Architecture Layers:
User Request → LiveView (UI only) → Context (business logic) → Schema/Repo (data)
↓ ↓ ↓
handle_event() Accounts.create_user() Repo.insert()elixir
# ✅ 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
endExample 3: Ecto N+1 Query Optimization
elixir
# ✅ CORRECT - Preload associations (2 queries total)
users = User |> Repo.all() |> Repo.preload(:posts)
Enum.map(users, fn user -> process(user, user.posts) end)
# Note: For complex filtering (e.g., WHERE posts.status = 'published'),
# use join + preload in the query itself. See EXTENDED.md for advanced patterns.
# ❌ 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
raiseelixir
# ❌ 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
endWhy: documents errors, pattern matching forces explicit handling.
@specDon't: Return nil
for Errors
nilelixir
# ❌ BAD - No context on failure
def find_user(email), do: Repo.get_by(User, email: email)
# ✅ CORRECT - Explicit error reason
@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
endDon't: Use =
Inside with
for Failable Operations
=withelixir
# ❌ BAD - Validate errors silenced
with {:ok, user} <- fetch_user(id),
validated = validate(user), # ← Doesn't check for {:error, _}
{:ok, saved} <- save(validated) do
{:ok, saved}
end
# ✅ CORRECT - All operations use <-
with {:ok, user} <- fetch_user(id),
{:ok, validated} <- validate(user),
{:ok, saved} <- save(validated) do
{:ok, saved}
endArchitecture & Boundaries
Don't: Put Business Logic in LiveView
elixir
# ❌ 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
case Repo.insert(User.changeset(%User{}, params)) do
{:ok, user} -> redirect(socket)
end
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
endWhy: Contexts testable without Phoenix, logic reusable.
Don't: Chain More Than 4 Steps in with
withelixir
# ❌ 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}
endData & Performance
Don't: Query Inside Loops (N+1)
elixir
# ❌ BAD - 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)
end)
# ✅ CORRECT - 2 queries total
User |> Repo.all() |> Repo.preload(:posts)Impact: 100 users with N+1 = 10 seconds vs 5ms with preload.
Don't: Query Without Indexes
elixir
# ❌ BAD - No index on frequently queried column
# Migration:
create table(:users) do
add :email, :string
end
# ✅ CORRECT - Add index
create table(:users) do
add :email, :string
end
create unique_index(:users, [:email])Why: Full table scan on 1M+ rows vs instant index lookup.
Testing
Don't: Write Tests Without Assertions
elixir
# ❌ BAD - What's being tested?
test "creates user" do
UserService.create_user(%{name: "Juan"})
end
# ✅ CORRECT - Assert expected behavior
test "creates user successfully" do
assert {:ok, user} = UserService.create_user(%{name: "Juan"})
assert user.name == "Juan"
endQuick Reference
| Situation | Anti-Pattern | Correct Pattern |
|---|---|---|
| Error handling | | |
| Missing data | Return | |
| Business logic | In LiveView | In context modules |
| Associations | | |
| with chains | | |
| Frequent queries | No index | |
| Testing | No assertions | |
| Complex logic | 6+ step | Group into 3 functions |
Resources
- Elixir Style Guide
- Phoenix Contexts
- Ecto Query Performance
- ExUnit Best Practices
- Extended patterns: See for 40+ anti-patterns
EXTENDED.md