# Idiomatic Elixir and Common Pitfalls — Elixir & Phoenix

Source: https://www.skillbyai.com/en/elixir-phoenix/s-idioms

> Write clear Elixir and avoid frequent mistakes.

## Writing Elixir like Elixirists

Idiomatic Elixir uses **pattern matching in function heads** instead of conditionals, **pipelines** for data transformation, **tagged tuples** with `case` and `with` for expected errors, small functions, **`@doc`** and **`@spec`** on public functions, and **supervised processes** for state and concurrency. Name boolean functions with `?` (`valid?`) and raising variants with `!`. Common pitfalls: using **processes for code organisation** rather than for runtime concerns (state, concurrency, fault isolation), which turns simple function calls into a bottleneck; a single GenServer handling everything; **unbounded mailboxes** when producers are faster than a consumer; creating **atoms from user input** with `String.to_atom` (atoms are not garbage-collected, so this can exhaust the atom table; use `String.to_existing_atom` or keep strings); deeply nested `case` statements instead of `with`; using `Enum` on huge data instead of `Stream`; forgetting that `GenServer.call` times out after 5 seconds by default; long-running work inside a LiveView or GenServer callback; and N+1 queries from preloading in loops. Rely on the formatter and Credo, and read HexDocs, as Elixir documentation is excellent.

## Unidiomatic versus idiomatic

The same logic with nested conditionals, then with pattern matching and with.

```elixir
defmodule Shop.Discounts do
  # unidiomatic: nested ifs and manual checks
  def apply_coupon_bad(order, code) do
    if order != nil do
      if Map.has_key?(order, :total) do
        coupon = String.to_atom(code)              # atoms from user input: dangerous
        if coupon == :festive10 do
          {:ok, Map.put(order, :total, order.total * 90 / 100)}
        else
          {:error, :invalid}
        end
      else
        {:error, :invalid}
      end
    else
      {:error, :invalid}
    end
  end

  # idiomatic: patterns, with and integer arithmetic
  @coupons %{"FESTIVE10" => 10, "WELCOME5" => 5}

  def apply_coupon(%{total: total} = order, code) when is_integer(total) do
    with {:ok, percent} <- Map.fetch(@coupons, String.upcase(code)) do
      {:ok, %{order | total: div(total * (100 - percent), 100)}}
    else
      :error -> {:error, :invalid_coupon}
    end
  end

  def apply_coupon(_order, _code), do: {:error, :invalid_order}
end
```

## Never create atoms from untrusted input

Atoms are stored in a global table that is never garbage-collected and has a fixed limit. Converting user input with `String.to_atom` can crash the VM; prefer strings, an explicit map or `String.to_existing_atom`.

**Quiz:** Why is String.to_atom on user input dangerous?

- [x] Atoms are not garbage-collected, so attackers can exhaust the atom table and crash the VM
- [ ] It is slow
- [ ] It returns nil
- [ ] It only works for numbers

*Answer:* Atoms are not garbage-collected, so attackers can exhaust the atom table and crash the VM. The atom table has a limit and atoms are never freed.
