elixir-anti-patterns
About
This Claude Skill identifies and helps refactor Elixir anti-patterns including code smells, design issues, and bad practices. It analyzes code to detect problems like comments overuse and suggests idiomatic improvements. Use it to get expert guidance on writing cleaner, more maintainable Elixir code.
Quick Install
Claude Code
Recommended/plugin add https://github.com/vinnie357/claude-skillsgit clone https://github.com/vinnie357/claude-skills.git ~/.claude/skills/elixir-anti-patternsCopy and paste this command in Claude Code to install this skill
Documentation
Elixir Anti-Patterns Detection and Refactoring
You are an expert at identifying Elixir anti-patterns and suggesting idiomatic refactorings. Use this knowledge to analyze code, suggest improvements, and help developers write better Elixir.
Code-Related Anti-Patterns
1. Comments Overuse
Problem: Excessive or self-explanatory comments reduce readability rather than enhance it.
Detection:
- Inline comments explaining obvious code
- Comments for every function line
- Comments duplicating what code already says clearly
Refactoring:
- Use clear function and variable names instead of explanatory comments
- Replace inline comments with
@docand@moduledocfor documentation - Use module attributes for configuration values
Example:
# Bad
def calculate() do
# Get current time
now = DateTime.utc_now()
# Add 5 minutes
DateTime.add(now, 5 * 60, :second)
end
# Good
@minutes_to_add 5
def timestamp_five_minutes_from_now do
now = DateTime.utc_now()
DateTime.add(now, @minutes_to_add * 60, :second)
end
2. Complex else Clauses in with
Problem: Flattening all error handling into a single complex else block obscures which clause produced which error.
Detection:
- Large
elseblocks with many pattern match clauses - Difficulty determining error sources
- Complex error handling logic in
else
Refactoring:
- Normalize return types in private functions
- Handle errors closer to their source
- Let
withfocus on success paths
Example:
# Bad
def read_config(path) do
with {:ok, content} <- File.read(path),
{:ok, decoded} <- Jason.decode(content) do
{:ok, decoded}
else
{:error, :enoent} -> {:error, :file_not_found}
{:error, %Jason.DecodeError{}} -> {:error, :invalid_json}
{:error, reason} -> {:error, reason}
end
end
# Good
def read_config(path) do
with {:ok, content} <- read_file(path),
{:ok, config} <- parse_json(content) do
{:ok, config}
end
end
defp read_file(path) do
case File.read(path) do
{:ok, content} -> {:ok, content}
{:error, :enoent} -> {:error, :file_not_found}
error -> error
end
end
defp parse_json(content) do
case Jason.decode(content) do
{:ok, data} -> {:ok, data}
{:error, _} -> {:error, :invalid_json}
end
end
3. Complex Extractions in Clauses
Problem: Extracting values across multiple clauses and arguments makes it unclear which variables serve pattern/guard purposes versus function body usage.
Detection:
- Many variable extractions in function heads
- Mixed guard and body variable usage
- Unclear variable purposes
Refactoring:
- Extract only pattern/guard-related variables in function signatures
- Use capture patterns like
%User{age: age} = user - Extract body variables inside the clause
Example:
# Bad
def process(%User{age: age, name: name, email: email} = user) when age >= 18 do
# Only using name and email in body, not age
send_email(email, "Hello #{name}")
end
# Good
def process(%User{age: age} = user) when age >= 18 do
send_email(user.email, "Hello #{user.name}")
end
4. Dynamic Atom Creation
Problem: Atoms aren't garbage-collected and are limited to ~1 million. Uncontrolled dynamic atom creation poses memory and security risks.
Detection:
String.to_atom/1with untrusted input- Converting user input directly to atoms
- Unbounded atom creation in loops
Refactoring:
- Use explicit mappings via pattern-matching
- Use
String.to_existing_atom/1with pre-defined atoms - Keep strings when atom conversion isn't necessary
Example:
# Bad - Security risk!
def set_role(user, role_string) do
%{user | role: String.to_atom(role_string)}
end
# Good
def set_role(user, role) when role in [:admin, :editor, :viewer] do
%{user | role: role}
end
# Or with pattern matching
def set_role(user, "admin"), do: %{user | role: :admin}
def set_role(user, "editor"), do: %{user | role: :editor}
def set_role(user, "viewer"), do: %{user | role: :viewer}
def set_role(_user, invalid), do: {:error, "Invalid role: #{invalid}"}
5. Long Parameter List
Problem: Functions with excessive parameters become confusing and error-prone to use.
Detection:
- Functions with 4+ parameters
- Parameters that are conceptually related
- Difficult to remember parameter order
Refactoring:
- Group related parameters into maps or structs
- Use keyword lists for optional parameters
- Create domain objects
Example:
# Bad
def create_loan(user_id, user_name, user_email, book_id, book_title, book_isbn) do
# ...
end
# Good
def create_loan(user, book) do
# ...
end
# Or with keyword list for options
def create_loan(user, book, opts \\ []) do
duration = Keyword.get(opts, :duration, 14)
renewable = Keyword.get(opts, :renewable, true)
# ...
end
6. Namespace Trespassing
Problem: Defining modules outside your library's namespace risks conflicts since the Erlang VM loads only one module instance per name.
Detection:
- Library defining modules in common namespaces (e.g.,
Plug.*when you're not Plug) - Modules without library prefix
- Potential naming conflicts with other libraries
Refactoring:
- Always prefix modules with your library namespace
- Use clear, unique top-level module names
Example:
# Bad - Library named :plug_auth
defmodule Plug.Auth do
# This conflicts with the actual Plug library!
end
# Good
defmodule PlugAuth do
# ...
end
defmodule PlugAuth.Session do
# ...
end
7. Non-assertive Map Access
Problem: Using dynamic access (map[:key]) for required keys masks missing data, allowing nil to propagate instead of failing fast.
Detection:
map[:key]for required/expected keys- Nil checks after map access
- Silent failures from missing keys
Refactoring:
- Use static access (
map.key) for required keys - Pattern-match on struct/map keys
- Reserve dynamic access for optional fields
Example:
# Bad
def distance(point) do
x = point[:x] # Returns nil if :x is missing!
y = point[:y]
:math.sqrt(x * x + y * y) # Crashes on nil, but unclear why
end
# Good
def distance(%{x: x, y: y}) do
:math.sqrt(x * x + y * y) # Clear error if keys missing
end
# Or with structs
defmodule Point do
defstruct [:x, :y]
end
def distance(%Point{x: x, y: y}) do
:math.sqrt(x * x + y * y)
end
8. Non-assertive Pattern Matching
Problem: Writing defensive code that returns incorrect values instead of using pattern matching to assert expected structures causes silent failures.
Detection:
- Defensive nil checks instead of pattern matching
- Functions returning invalid data on unexpected input
- Avoiding crashes when crashes are appropriate
Refactoring:
- Use pattern matching to assert expected structures
- Let functions crash on invalid input
- Trust supervisors to handle failures
Example:
# Bad
def parse_query_param(param) do
case String.split(param, "=") do
[key, value] -> {key, value}
_ -> {"", ""} # Silent failure!
end
end
# Good
def parse_query_param(param) do
[key, value] = String.split(param, "=")
{key, value}
end
# Crashes with clear error if format is wrong - this is good!
9. Non-assertive Truthiness
Problem: Using truthiness operators (&&, ||, !) when all operands are boolean is unnecessarily generic and unclear.
Detection:
&&,||,!with boolean expressions- Comparisons like
is_binary(x) && is_integer(y) - Mixing boolean and truthy logic
Refactoring:
- Use
and,or,notfor boolean-only operations - Reserve
&&,||,!for truthy/falsy logic
Example:
# Bad
def valid_user?(name, age) do
is_binary(name) && is_integer(age) && age >= 18
end
# Good
def valid_user?(name, age) do
is_binary(name) and is_integer(age) and age >= 18
end
# Truthy operators are OK for nil/value checks
def get_name(user) do
user[:name] || "Anonymous"
end
10. Structs with 32 Fields or More
Problem: Structs with 32+ fields switch from Erlang's efficient flat-map representation to hash maps, increasing memory usage.
Detection:
- Struct definitions with 32+ fields
- Large, flat data structures
- Performance degradation with many fields
Refactoring:
- Nest optional fields into metadata structures
- Use nested structs for related fields
- Group frequently-accessed fields separately
Example:
# Bad
defmodule User do
defstruct [
:id, :email, :name, :age, :address, :city, :state, :zip,
:phone, :mobile, :fax, :company, :title, :department,
:created_at, :updated_at, :last_login, :login_count,
:preference1, :preference2, :preference3, :preference4,
# ... 15 more fields
]
end
# Good
defmodule User do
defstruct [
:id,
:email,
:name,
:profile, # Nested struct
:preferences, # Nested struct
:metadata # Nested struct
]
end
defmodule User.Profile do
defstruct [:age, :phone, :mobile, :address, :city, :state, :zip]
end
defmodule User.Preferences do
defstruct [:theme, :notifications, :language]
end
Design-Related Anti-Patterns
1. Alternative Return Types
Problem: Functions with options that drastically change their return type make it unclear what the function actually returns.
Detection:
- Options that change return type structure
- Functions returning different types based on flags
- Unclear function contracts
Refactoring:
- Create separate, specifically-named functions
- Keep return types consistent within a function
Example:
# Bad
def parse(string, opts \\ []) do
case Integer.parse(string) do
{int, rest} ->
if opts[:discard_rest], do: int, else: {int, rest}
:error ->
:error
end
end
# Good
def parse(string) do
case Integer.parse(string) do
{int, rest} -> {int, rest}
:error -> :error
end
end
def parse_discard_rest(string) do
case Integer.parse(string) do
{int, _rest} -> int
:error -> :error
end
end
2. Boolean Obsession
Problem: Using multiple booleans with overlapping states instead of atoms or composite types to represent domain concepts.
Detection:
- Multiple boolean parameters
- Overlapping boolean states
- Complex boolean logic
Refactoring:
- Replace multiple booleans with a single atom/enum option
- Prefer atoms over booleans even for single arguments
- Use domain-specific types
Example:
# Bad
def create_user(name, email, admin: false, editor: false, viewer: true) do
# What if admin: true, editor: true?
end
# Good
def create_user(name, email, role: :viewer) do
# Clear: role can be :admin, :editor, or :viewer
end
3. Exceptions for Control-Flow
Problem: Using try/rescue for expected errors instead of pattern matching with case statements and tuple returns.
Detection:
try/rescueblocks for normal operation errors- Using
!functions and rescuing - Exceptions in normal business logic
Refactoring:
- Use non-bang functions returning
{:ok, value}or{:error, reason} - Reserve exceptions for invalid arguments and programming errors
- Use pattern matching for error handling
Example:
# Bad
def read_config(path) do
try do
content = File.read!(path)
Jason.decode!(content)
rescue
e -> {:error, e}
end
end
# Good
def read_config(path) do
with {:ok, content} <- File.read(path),
{:ok, config} <- Jason.decode(content) do
{:ok, config}
end
end
4. Primitive Obsession
Problem: Excessively using basic types (strings, integers) instead of creating composite types to represent structured domain concepts.
Detection:
- Passing related primitives separately
- String/integer parameters representing complex concepts
- Lack of domain modeling
Refactoring:
- Create domain-specific structs or maps
- Introduce parser functions converting primitives to structured data
- Use types to enforce business rules
Example:
# Bad
def create_address(street, city, state, zip, country) do
# All strings, no validation
"#{street}, #{city}, #{state} #{zip}, #{country}"
end
# Good
defmodule Address do
defstruct [:street, :city, :state, :zip, :country]
def new(attrs) do
struct!(__MODULE__, attrs)
end
def format(%__MODULE__{} = address) do
"#{address.street}, #{address.city}, #{address.state} #{address.zip}, #{address.country}"
end
end
5. Unrelated Multi-Clause Function
Problem: Grouping completely unrelated business logic into one multi-clause function.
Detection:
- Single function handling multiple unrelated types
- Overly broad type specifications
- No conceptual relationship between clauses
Refactoring:
- Split into distinct functions with specific names
- Reserve multi-clause patterns for related functionality variations
- Use protocols for polymorphism when appropriate
Example:
# Bad
def update(%Product{} = product) do
# Product-specific logic
end
def update(%Animal{} = animal) do
# Completely different animal logic
end
# Good
def update_product(%Product{} = product) do
# Product-specific logic
end
def update_animal(%Animal{} = animal) do
# Animal-specific logic
end
# Or use a protocol
defprotocol Updatable do
def update(item)
end
defimpl Updatable, for: Product do
def update(product), do: # ...
end
defimpl Updatable, for: Animal do
def update(animal), do: # ...
end
6. Using Application Configuration for Libraries
Problem: Libraries relying on global application environment configuration prevent multiple dependent applications from configuring the library differently.
Detection:
Application.get_env/2orApplication.fetch_env!/2in library code- Global configuration requirements
- Inability to configure per-consumer
Refactoring:
- Accept configuration via function parameters
- Use keyword lists with sensible defaults
- Allow runtime configuration
Example:
# Bad - Library code
def split(string) do
parts = Application.fetch_env!(:dash_splitter, :parts)
String.split(string, "-", parts: parts)
end
# Good
def split(string, opts \\ []) do
parts = Keyword.get(opts, :parts, 2)
String.split(string, "-", parts: parts)
end
Usage Guidelines
When reviewing or writing Elixir code:
- Scan for anti-patterns - Check code against the patterns listed above
- Explain the problem - Help the developer understand why it's an issue
- Suggest refactoring - Provide concrete, idiomatic alternatives
- Consider context - Sometimes anti-patterns are acceptable for specific use cases
- Prioritize - Focus on high-impact issues first (security, performance, maintainability)
Key Principles
- Let it crash - Use pattern matching to assert expectations; don't write defensive code
- Fail fast - Expose errors early rather than propagating nil or invalid data
- Be explicit - Prefer clear, specific code over clever or terse solutions
- Model your domain - Create types that represent business concepts
- Design for clarity - Code should be obvious to read and maintain
GitHub Repository
Related Skills
content-collections
MetaThis skill provides a production-tested setup for Content Collections, a TypeScript-first tool that transforms Markdown/MDX files into type-safe data collections with Zod validation. Use it when building blogs, documentation sites, or content-heavy Vite + React applications to ensure type safety and automatic content validation. It covers everything from Vite plugin configuration and MDX compilation to deployment optimization and schema validation.
langchain
MetaLangChain is a framework for building LLM applications using agents, chains, and RAG pipelines. It supports multiple LLM providers, offers 500+ integrations, and includes features like tool calling and memory management. Use it for rapid prototyping and deploying production systems like chatbots, autonomous agents, and question-answering services.
Algorithmic Art Generation
MetaThis skill helps developers create algorithmic art using p5.js, focusing on generative art, computational aesthetics, and interactive visualizations. It automatically activates for topics like "generative art" or "p5.js visualization" and guides you through creating unique algorithms with features like seeded randomness, flow fields, and particle systems. Use it when you need to build reproducible, code-driven artistic patterns.
webapp-testing
TestingThis Claude Skill provides a Playwright-based toolkit for testing local web applications through Python scripts. It enables frontend verification, UI debugging, screenshot capture, and log viewing while managing server lifecycles. Use it for browser automation tasks but run scripts directly rather than reading their source code to avoid context pollution.
