Lesson 24 / 25
Idiomatic Elixir and Common Pitfalls
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.
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}
endNever 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.
Quick check: Why is String.to_atom on user input dangerous?
- 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.