Introduction
Getting Started
- QuickStart
Patterns
- Languages
- Supported Languages
- Python
- Java
- JavaScript
- TypeScript
- Node.js
- React
- Fastify
- Next.js
- Terraform
- C#
- C++
- C
- Go
- Rust
- Swift
- React Native
- Spring Boot
- Kotlin
- Flutter
- Ruby
- PHP
- Scala
- Perl
- R
- Dart
- Elixir
- Erlang
- Haskell
- Lua
- Julia
- Clojure
- Groovy
- Fortran
- COBOL
- Pascal
- Assembly
- Bash
- PowerShell
- SQL
- PL/SQL
- T-SQL
- MATLAB
- Objective-C
- VBA
- ABAP
- Apex
- Apache Camel
- Crystal
- D
- Delphi
- Elm
- F#
- Hack
- Lisp
- OCaml
- Prolog
- Racket
- Scheme
- Solidity
- Verilog
- VHDL
- Zig
- MongoDB
- ClickHouse
- MySQL
- GraphQL
- Redis
- Cassandra
- Elasticsearch
- Security
- Performance
Integrations
- Code Repositories
- Team Messengers
- Ticketing
Enterprise
Elixir is a dynamic, functional programming language designed for building scalable and maintainable applications. It leverages the Erlang VM, known for running low-latency, distributed, and fault-tolerant systems.
Elixir, despite being a well-designed functional language, has several common anti-patterns that can lead to performance issues, maintainability problems, and bugs. Here are the most important anti-patterns to avoid when writing Elixir code.
# Anti-pattern: Using processes unnecessarily
defmodule Calculator do
def start do
spawn(fn -> loop(%{}) end)
end
def loop(state) do
receive do
{:add, a, b, caller} ->
send(caller, {:result, a + b})
loop(state)
# Other operations...
end
end
end
# Usage
calculator = Calculator.start()
send(calculator, {:add, 2, 3, self()})
receive do
{:result, value} -> IO.puts("Result: #{value}")
end
# Better approach: Use regular functions for simple operations
defmodule Calculator do
def add(a, b), do: a + b
# Other operations...
end
# Usage
result = Calculator.add(2, 3)
IO.puts("Result: #{result}")
Don’t use processes for simple operations that don’t need concurrency, state isolation, or fault tolerance. Processes have overhead and should be used when their benefits are needed.
# Anti-pattern: Reinventing OTP patterns
defmodule UserStore do
def start do
spawn(fn -> loop(%{}) end)
end
def loop(state) do
receive do
{:get, key, caller} ->
send(caller, {:ok, Map.get(state, key)})
loop(state)
{:put, key, value} ->
loop(Map.put(state, key, value))
# No proper error handling, supervision, etc.
end
end
end
# Better approach: Use GenServer
defmodule UserStore do
use GenServer
# Client API
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, %{}, opts)
end
def get(pid, key) do
GenServer.call(pid, {:get, key})
end
def put(pid, key, value) do
GenServer.cast(pid, {:put, key, value})
end
# Server callbacks
@impl true
def init(state) do
{:ok, state}
end
@impl true
def handle_call({:get, key}, _from, state) do
{:reply, Map.get(state, key), state}
end
@impl true
def handle_cast({:put, key, value}, state) do
{:noreply, Map.put(state, key, value)}
end
end
Use OTP behaviors like GenServer, Supervisor, and Application instead of reinventing them. They provide battle-tested solutions for common patterns.
# Anti-pattern: Using Agent for complex state management
defmodule ComplexStore do
def start_link do
Agent.start_link(fn -> %{} end)
end
def get_user_posts(agent, user_id) do
Agent.get(agent, fn state ->
# Complex data processing inside Agent
posts = Map.get(state, :posts, %{})
user_posts = Map.get(posts, user_id, [])
Enum.filter(user_posts, &(&1.published))
|> Enum.sort_by(&(&1.date), {:desc, Date})
end)
end
def add_post(agent, user_id, post) do
Agent.update(agent, fn state ->
posts = Map.get(state, :posts, %{})
user_posts = Map.get(posts, user_id, [])
updated_posts = [post | user_posts]
Map.put(state, :posts, Map.put(posts, user_id, updated_posts))
end)
end
end
# Better approach: Use GenServer for complex state
defmodule ComplexStore do
use GenServer
# Client API
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, %{}, opts)
end
def get_user_posts(pid, user_id) do
GenServer.call(pid, {:get_user_posts, user_id})
end
def add_post(pid, user_id, post) do
GenServer.cast(pid, {:add_post, user_id, post})
end
# Server callbacks
@impl true
def init(state) do
{:ok, state}
end
@impl true
def handle_call({:get_user_posts, user_id}, _from, state) do
posts = Map.get(state, :posts, %{})
user_posts = Map.get(posts, user_id, [])
result = Enum.filter(user_posts, &(&1.published))
|> Enum.sort_by(&(&1.date), {:desc, Date})
{:reply, result, state}
end
@impl true
def handle_cast({:add_post, user_id, post}, state) do
posts = Map.get(state, :posts, %{})
user_posts = Map.get(posts, user_id, [])
updated_posts = [post | user_posts]
new_state = Map.put(state, :posts, Map.put(posts, user_id, updated_posts))
{:noreply, new_state}
end
end
Use Agent only for simple state. For complex state management with custom logic, use GenServer which gives you more control and better performance.
# Anti-pattern: Not using pattern matching
def process_response(response) do
if response.status == :ok do
data = response.body
# Process data
else
error = response.error
# Handle error
end
end
# Better approach: Use pattern matching
def process_response({:ok, body}) do
# Process data
end
def process_response({:error, reason}) do
# Handle error
end
Leverage Elixir’s pattern matching for cleaner, more declarative code. Use it in function heads, case statements, and with expressions.
# Anti-pattern: Using strings for map keys
user = %{"name" => "John", "age" => 30}
name = user["name"]
# Better approach: Use atoms for map keys
user = %{name: "John", age: 30}
name = user.name # More concise access
Use atoms for map keys when the set of keys is known and limited. Atoms are more efficient and allow for the dot notation. However, be careful not to create atoms dynamically from user input as they are not garbage collected.
# Anti-pattern: Nested case statements
def process_data(data) do
case parse_data(data) do
{:ok, parsed} ->
case validate_data(parsed) do
{:ok, validated} ->
case save_data(validated) do
{:ok, result} -> {:ok, result}
{:error, reason} -> {:error, reason}
end
{:error, reason} -> {:error, reason}
end
{:error, reason} -> {:error, reason}
end
end
# Better approach: Use with
def process_data(data) do
with {:ok, parsed} <- parse_data(data),
{:ok, validated} <- validate_data(parsed),
{:ok, result} <- save_data(validated) do
{:ok, result}
end
end
Use with
expressions for sequences of operations where each step depends on the success of the previous one. It makes the code more readable and avoids the “pyramid of doom”.
# Anti-pattern: Type checking
def stringify(value) do
cond do
is_integer(value) -> Integer.to_string(value)
is_float(value) -> Float.to_string(value)
is_list(value) -> Enum.join(value, ", ")
true -> raise "Unsupported type"
end
end
# Better approach: Use protocols
defprotocol Stringify do
def to_string(value)
end
defimpl Stringify, for: Integer do
def to_string(value), do: Integer.to_string(value)
end
defimpl Stringify, for: Float do
def to_string(value), do: Float.to_string(value)
end
defimpl Stringify, for: List do
def to_string(value), do: Enum.join(value, ", ")
end
Use protocols for polymorphic behavior instead of type checking. They provide a clean way to implement behavior that varies based on type.
# Anti-pattern: Inefficient list concatenation
def process_items(items) do
Enum.reduce(items, [], fn item, acc ->
processed = process_item(item)
acc ++ [processed] # Inefficient: O(n) operation
end)
end
# Better approach: Use list prepending and reverse
def process_items(items) do
items
|> Enum.reduce([], fn item, acc ->
processed = process_item(item)
[processed | acc] # Efficient: O(1) operation
end)
|> Enum.reverse() # Reverse once at the end
end
Avoid using ++
for list concatenation in loops. Instead, use list prepending ([head | tail]
) and reverse the result at the end if order matters.
# Anti-pattern: Eager evaluation of large collections
def process_large_file(file_path) do
File.stream!(file_path)
|> Enum.map(&String.trim/1)
|> Enum.filter(&(String.length(&1) > 0))
|> Enum.map(&process_line/1)
|> Enum.take(10)
end
# Better approach: Use Stream for lazy evaluation
def process_large_file(file_path) do
File.stream!(file_path)
|> Stream.map(&String.trim/1)
|> Stream.filter(&(String.length(&1) > 0))
|> Stream.map(&process_line/1)
|> Enum.take(10) # Only now is the stream evaluated
end
Use Stream
instead of Enum
for large collections or when you only need part of the result. Streams are lazy and avoid unnecessary computation.
# Anti-pattern: Manual process management
defmodule MyApp do
def start do
database_pid = Database.start_link()
cache_pid = Cache.start_link()
api_pid = API.start_link(database_pid, cache_pid)
# What if one of these crashes?
end
end
# Better approach: Use supervision trees
defmodule MyApp.Application do
use Application
def start(_type, _args) do
children = [
MyApp.Database,
MyApp.Cache,
{MyApp.API, []}
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
end
Use proper supervision trees to manage process lifecycles and handle failures. This is a core strength of the BEAM VM and Elixir’s OTP framework.
# Anti-pattern: Not awaiting tasks
def process_items(items) do
items
|> Enum.map(fn item ->
Task.async(fn -> process_item(item) end)
end)
# Tasks are started but never awaited
end
# Better approach: Properly await tasks
def process_items(items) do
tasks = Enum.map(items, fn item ->
Task.async(fn -> process_item(item) end)
end)
Enum.map(tasks, &Task.await/1)
end
Always call Task.await/1
or Task.await/2
on tasks started with Task.async/1
to avoid memory leaks and ensure proper error propagation.
# Anti-pattern: Manual testing
defmodule Calculator do
def add(a, b), do: a + b
end
# Manual test
IO.puts("Testing add...")
result = Calculator.add(2, 3)
if result == 5 do
IO.puts("Test passed!")
else
IO.puts("Test failed!")
end
# Better approach: Use ExUnit
defmodule CalculatorTest do
use ExUnit.Case
test "adds two numbers" do
assert Calculator.add(2, 3) == 5
end
test "handles negative numbers" do
assert Calculator.add(-2, 3) == 1
assert Calculator.add(2, -3) == -1
end
end
Use ExUnit for testing instead of writing manual test code. It provides a structured way to write and run tests with helpful assertions and reporting.
# Anti-pattern: No typespecs
defmodule User do
def create(name, age) do
%{name: name, age: age}
end
def adult?(user) do
user.age >= 18
end
end
# Better approach: Use typespecs
defmodule User do
@type t :: %{name: String.t(), age: non_neg_integer()}
@spec create(String.t(), non_neg_integer()) :: t()
def create(name, age) do
%{name: name, age: age}
end
@spec adult?(t()) :: boolean()
def adult?(user) do
user.age >= 18
end
end
Use typespecs to document function signatures and data structures. They improve code documentation and allow tools like Dialyzer to perform static analysis.
# Anti-pattern: Excessive use of try/rescue
def divide(a, b) do
try do
a / b
rescue
ArithmeticError -> 0
end
end
# Better approach: Use pattern matching and guard clauses
def divide(a, _b = 0), do: {:error, :division_by_zero}
def divide(a, b), do: {:ok, a / b}
Avoid using try/rescue
for control flow. In Elixir, it’s more idiomatic to use pattern matching, guard clauses, and tagged tuples for error handling.
# Anti-pattern: Poor module structure
defmodule UserSystem do
# Hundreds of functions in one module
def create_user(params), do: # ...
def update_user(id, params), do: # ...
def delete_user(id), do: # ...
def get_user(id), do: # ...
def list_users, do: # ...
def authenticate_user(email, password), do: # ...
def generate_password_reset_token(email), do: # ...
# And many more...
end
# Better approach: Proper module organization
defmodule MyApp.Accounts do
# Public API for account management
def create_user(params), do: # ...
def update_user(id, params), do: # ...
def delete_user(id), do: # ...
def get_user(id), do: # ...
def list_users, do: # ...
end
defmodule MyApp.Accounts.Authentication do
# Authentication-specific functionality
def authenticate_user(email, password), do: # ...
def generate_password_reset_token(email), do: # ...
end
defmodule MyApp.Accounts.User do
# User struct and validations
defstruct [:id, :name, :email, :password_hash]
def changeset(user, params), do: # ...
end
Organize your code into cohesive modules with clear responsibilities. Follow the principle of single responsibility and create a logical hierarchy of modules.
# Anti-pattern: Inconsistent error handling
def create_user(params) do
if valid_params?(params) do
# Create user
%{id: 123, name: params.name}
else
nil # What went wrong?
end
end
# Usage
user = create_user(params)
if user do
# Success
else
# Error, but what kind?
end
# Better approach: Use tagged tuples
def create_user(params) do
case validate_params(params) do
:ok ->
# Create user
{:ok, %{id: 123, name: params.name}}
{:error, reason} ->
{:error, reason}
end
end
# Usage
case create_user(params) do
{:ok, user} ->
# Success
{:error, :invalid_email} ->
# Handle specific error
{:error, reason} ->
# Handle other errors
end
Use tagged tuples like {:ok, result}
and {:error, reason}
for functions that can fail. This makes error handling explicit and allows for pattern matching on specific error types.
# Anti-pattern: Poor or no documentation
defmodule UserAPI do
def create(params) do
# Implementation...
end
end
# Better approach: Proper documentation
defmodule UserAPI do
@moduledoc """
Provides functions for managing users in the system.
"""
@doc """
Creates a new user with the given parameters.
## Parameters
* `params` - A map containing user attributes:
* `:name` - The user's full name (required)
* `:email` - The user's email address (required)
* `:age` - The user's age (optional)
## Examples
iex> UserAPI.create(%{name: "John Doe", email: "john@example.com"})
{:ok, %User{id: 123, name: "John Doe", email: "john@example.com"}}
iex> UserAPI.create(%{name: "John Doe"})
{:error, :email_required}
## Returns
* `{:ok, user}` if the user was created successfully
* `{:error, reason}` if there was an error
"""
def create(params) do
# Implementation...
end
end
Write comprehensive documentation for your modules and functions using @moduledoc
and @doc
attributes. Include examples, parameter descriptions, and return value information.
# Anti-pattern: String interpolation for SQL queries
def find_users(name, min_age) do
query = "SELECT * FROM users WHERE name LIKE '%#{name}%' AND age >= #{min_age}"
# Execute query...
end
# Better approach: Use parameterized queries
def find_users(name, min_age) do
query = "SELECT * FROM users WHERE name LIKE $1 AND age >= $2"
params = ["%#{name}%", min_age]
# Execute query with params...
end
# Even better with Ecto
def find_users(name, min_age) do
from(u in User,
where: like(u.name, ^"%#{name}%") and u.age >= ^min_age
)
|> Repo.all()
end
Never use string interpolation for building database queries. Use parameterized queries or query builders like Ecto to prevent SQL injection attacks.
# Anti-pattern: Hardcoded configuration
defmodule MyApp.API do
def base_url do
"https://api.example.com"
end
def api_key do
"hardcoded_api_key"
end
end
# Better approach: Use application configuration
# In config/config.exs
config :my_app, :api,
base_url: "https://api.example.com",
api_key: System.get_env("API_KEY")
# In your module
defmodule MyApp.API do
def base_url do
Application.get_env(:my_app, :api)[:base_url]
end
def api_key do
Application.get_env(:my_app, :api)[:api_key]
end
end
Use Elixir’s application configuration system instead of hardcoding configuration values. This allows for different configurations in different environments.