elixir-essentials

MANDATORY for ALL Elixir code changes. Invoke before writing any .ex or .exs file.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "elixir-essentials" with this command: npx skills add j-morgan6/elixir-claude-optimization/j-morgan6-elixir-claude-optimization-elixir-essentials

Elixir Essentials

RULES — Follow these with no exceptions

  1. Use pattern matching over if/else for control flow and data extraction
  2. Add @impl true before every callback function (mount, handle_event, handle_info, etc.)
  3. Return {:ok, result} | {:error, reason} tuples for fallible operations
  4. Use with for 2+ sequential fallible operations instead of nested case
  5. Use the pipe operator for 2+ chained transformations
  6. Never nest if/else statements — use case, cond, or multi-clause functions
  7. Predicate functions end with ?, dangerous functions end with !
  8. Let it crash — don't write defensive code for impossible states

Pattern Matching

Pattern matching is the primary control flow mechanism in Elixir. Prefer it over conditional statements.

Prefer Pattern Matching Over if/else

Bad:

def process(result) do
  if result.status == :ok do
    result.data
  else
    nil
  end
end

Good:

def process(%{status: :ok, data: data}), do: data
def process(_), do: nil

Use Case for Multiple Patterns

Bad:

def handle_response(response) do
  if response.status == 200 do
    {:ok, response.body}
  else if response.status == 404 do
    {:error, :not_found}
  else
    {:error, :unknown}
  end
end

Good:

def handle_response(%{status: 200, body: body}), do: {:ok, body}
def handle_response(%{status: 404}), do: {:error, :not_found}
def handle_response(_), do: {:error, :unknown}

Pipe Operator

Use the pipe operator |> to chain function calls for improved readability.

Basic Piping

Bad:

String.upcase(String.trim(user_input))

Good:

user_input
|> String.trim()
|> String.upcase()

Pipe into Function Heads

Bad:

def process_user(user) do
  validated = validate_user(user)
  transformed = transform_user(validated)
  save_user(transformed)
end

Good:

def process_user(user) do
  user
  |> validate_user()
  |> transform_user()
  |> save_user()
end

With Statement

Use with for sequential operations that can fail.

Bad:

def create_post(params) do
  case validate_params(params) do
    {:ok, valid_params} ->
      case create_changeset(valid_params) do
        {:ok, changeset} ->
          Repo.insert(changeset)
        error -> error
      end
    error -> error
  end
end

Good:

def create_post(params) do
  with {:ok, valid_params} <- validate_params(params),
       {:ok, changeset} <- create_changeset(valid_params),
       {:ok, post} <- Repo.insert(changeset) do
    {:ok, post}
  end
end

With Statement - Inline Error Handling

Handle specific errors in the else block.

def transfer_money(from_id, to_id, amount) do
  with {:ok, from_account} <- get_account(from_id),
       {:ok, to_account} <- get_account(to_id),
       :ok <- validate_balance(from_account, amount),
       {:ok, _} <- debit(from_account, amount),
       {:ok, _} <- credit(to_account, amount) do
    {:ok, :transfer_complete}
  else
    {:error, :insufficient_funds} ->
      {:error, "Not enough money in account"}

    {:error, :not_found} ->
      {:error, "Account not found"}

    error ->
      {:error, "Transfer failed: #{inspect(error)}"}
  end
end

Guards

Use guards for simple type and value checks in function heads.

def calculate(x) when is_integer(x) and x > 0 do
  x * 2
end

def calculate(_), do: {:error, :invalid_input}

List Comprehensions

Use for comprehensions for complex transformations and filtering.

Bad (multiple passes):

list
|> Enum.map(&transform/1)
|> Enum.filter(&valid?/1)
|> Enum.map(&format/1)

Good (single pass):

for item <- list,
    transformed = transform(item),
    valid?(transformed) do
  format(transformed)
end

Naming Conventions

  • Module names: PascalCase
  • Function names: snake_case
  • Variables: snake_case
  • Atoms: :snake_case
  • Predicate functions end with ?: valid?, empty?
  • Dangerous functions end with !: save!, update!

Tagged Tuples for Error Handling

The idiomatic way to handle success and failure in Elixir.

def fetch_user(id) do
  case Repo.get(User, id) do
    nil -> {:error, :not_found}
    user -> {:ok, user}
  end
end

# Usage
case fetch_user(123) do
  {:ok, user} -> IO.puts("Found: #{user.name}")
  {:error, :not_found} -> IO.puts("User not found")
end

Case Statements

Pattern match on results.

def process_upload(file) do
  case save_file(file) do
    {:ok, path} ->
      Logger.info("File saved to #{path}")
      create_record(path)

    {:error, :invalid_format} ->
      {:error, "File format not supported"}

    {:error, reason} ->
      Logger.error("Upload failed: #{inspect(reason)}")
      {:error, "Upload failed"}
  end
end

Bang Functions

Functions ending with ! raise errors instead of returning tuples.

# Returns {:ok, user} or {:error, changeset}
def create_user(attrs) do
  %User{}
  |> User.changeset(attrs)
  |> Repo.insert()
end

# Returns user or raises
def create_user!(attrs) do
  %User{}
  |> User.changeset(attrs)
  |> Repo.insert!()
end

# Usage
try do
  user = create_user!(invalid_attrs)
  IO.puts("Created #{user.name}")
rescue
  e in Ecto.InvalidChangesetError ->
    IO.puts("Failed: #{inspect(e)}")
end

Try/Rescue

Catch exceptions when needed (use sparingly).

def parse_json(string) do
  try do
    {:ok, Jason.decode!(string)}
  rescue
    Jason.DecodeError -> {:error, :invalid_json}
  end
end

Supervision Trees

Let processes fail and restart (preferred over defensive coding).

defmodule MyApp.Application do
  use Application

  def start(_type, _args) do
    children = [
      MyApp.Repo,
      MyAppWeb.Endpoint,
      {MyApp.Worker, []}
    ]

    opts = [strategy: :one_for_one, name: MyApp.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

GenServer Error Handling

Handle errors in GenServer callbacks.

def handle_call(:risky_operation, _from, state) do
  case perform_operation() do
    {:ok, result} ->
      {:reply, {:ok, result}, update_state(state, result)}

    {:error, reason} ->
      Logger.error("Operation failed: #{inspect(reason)}")
      {:reply, {:error, reason}, state}
  end
end

# Let it crash for unexpected errors
def handle_cast(:dangerous_work, state) do
  # If this raises, supervisor will restart the process
  result = dangerous_function!()
  {:noreply, Map.put(state, :result, result)}
end

Validation Errors

Return clear, actionable error messages.

def validate_image_upload(file) do
  with :ok <- validate_file_type(file),
       :ok <- validate_file_size(file),
       :ok <- validate_dimensions(file) do
    {:ok, file}
  else
    {:error, :invalid_type} ->
      {:error, "Only JPEG, PNG, and GIF files are allowed"}

    {:error, :too_large} ->
      {:error, "File must be less than 10MB"}

    {:error, :invalid_dimensions} ->
      {:error, "Image must be at least 100x100 pixels"}
  end
end

Changeset Errors

Extract and format Ecto changeset errors.

def changeset_errors(changeset) do
  Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} ->
    Enum.reduce(opts, msg, fn {key, value}, acc ->
      String.replace(acc, "%{#{key}}", to_string(value))
    end)
  end)
end

# Usage
case create_user(attrs) do
  {:ok, user} -> {:ok, user}
  {:error, changeset} ->
    errors = changeset_errors(changeset)
    {:error, errors}
end

Early Returns

Use pattern matching in function heads for early returns.

def process_data(nil), do: {:error, :no_data}
def process_data([]), do: {:error, :empty_list}
def process_data(data) when is_list(data) do
  # Process the list
  {:ok, Enum.map(data, &transform/1)}
end

Avoid Defensive Programming

Don't check for things that can't happen. Let it crash.

Bad (defensive):

def get_username(user) do
  if user && user.name do
    user.name
  else
    "Unknown"
  end
end

Good (trust your types):

def get_username(%User{name: name}), do: name

If the user is nil or doesn't have a name, it's a bug that should crash and be fixed.

Documentation

Use @doc for public functions and @moduledoc for modules.

defmodule MyModule do
  @moduledoc """
  This module handles user operations.
  """

  @doc """
  Fetches a user by ID.

  Returns `{:ok, user}` or `{:error, :not_found}`.
  """
  def fetch_user(id), do: # ...
end

Immutability

All data structures are immutable. Functions return new values rather than modifying in place.

# Always returns a new list
list = [1, 2, 3]
new_list = [0 | list]  # [0, 1, 2, 3]
# list is still [1, 2, 3]

Testing

When writing test files for Elixir modules, invoke elixir-phoenix-guide:testing-essentials before writing any _test.exs file.

Anonymous Functions

Use the capture operator & for concise anonymous functions.

Verbose:

Enum.map(list, fn x -> x * 2 end)

Concise:

Enum.map(list, &(&1 * 2))

Named function capture:

Enum.map(users, &User.format/1)

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

phoenix-uploads

No summary provided by upstream source.

Repository SourceNeeds Review
General

phoenix-liveview

No summary provided by upstream source.

Repository SourceNeeds Review
General

phoenix-liveview-essentials

No summary provided by upstream source.

Repository SourceNeeds Review