Elixir Best Practices and Common Mistakes
A closing checklist of habits that separate idiomatic, maintainable Elixir from code that merely runs correctly.
3 min read
Elixir code can "work" while still fighting the language's own idioms — treating it like a mutable, object-oriented language in disguise instead of leaning into the patterns it's actually built around. Here's a checklist worth returning to.
Common mistakes to avoid
Reaching for cond when case or multiple function clauses would pattern-match more precisely.
# Avoid
def handle(result) do
cond do
elem(result, 0) == :ok -> "success"
elem(result, 0) == :error -> "failure"
end
end
# Prefer
def handle({:ok, _}), do: "success"
def handle({:error, _}), do: "failure"Overusing try/rescue where a tagged tuple return is more idiomatic.
# Avoid
def parse(input) do
try do
{:ok, String.to_integer(input)}
rescue
_ -> {:error, :invalid}
end
end
# Prefer -- this specific case has a built-in safe function already
Integer.parse(input)Deeply nested case statements instead of with.
# Avoid
case step_one() do
{:ok, a} ->
case step_two(a) do
{:ok, b} -> {:ok, b}
error -> error
end
error -> error
end
# Prefer
with {:ok, a} <- step_one(),
{:ok, b} <- step_two(a) do
{:ok, b}
endUsing Enum when Stream would avoid building intermediate lists.
# Avoid -- builds a full intermediate list at every step
1..1_000_000
|> Enum.map(&(&1 * 2))
|> Enum.filter(&(&1 > 100))
|> Enum.take(5)
# Prefer -- lazy, only computes what's needed
1..1_000_000
|> Stream.map(&(&1 * 2))
|> Stream.filter(&(&1 > 100))
|> Enum.take(5)Spawning raw processes for state that needs supervision.
Ad hoc spawn calls for anything long-lived and stateful skip the restart guarantees a GenServer under a Supervisor gives you for free — reach for OTP behaviours rather than hand-rolled process loops outside of learning exercises.
Ignoring compiler warnings.
Elixir's compiler warns about unused variables, unreachable clauses, and undefined functions for good reason — an unused variable warning is often the first sign a pattern match isn't doing what you think it is.
A final checklist
- [ ] Functions that can fail return
{:ok, _}/{:error, _}, not exceptions, unless the failure is truly exceptional. - [ ] Multiple function clauses or
caseare used instead of acondchain checking a single value's shape. - [ ] Multi-step fallible logic uses
withinstead of nestedcase. - [ ] Data being transformed flows through
|>pipelines rather than deeply nested calls. - [ ] Long-lived, stateful processes are
GenServers under aSupervisor, not rawspawn. - [ ] Modules and public functions have
@moduledoc/@doc(runmix docsto confirm they render). - [ ]
mix formathas been run, andmix credo(if configured) passes without new warnings. - [ ] No compiler warnings on
mix compile --warnings-as-errors.
None of these are exotic techniques — they're the same constructs covered throughout this course, applied with a bit more discipline. That discipline, and genuinely trusting the "let it crash" model instead of fighting it with defensive code everywhere, is most of what separates Elixir code that merely runs from Elixir code that reads and scales the way the language was designed for.
Test what you just learned
4 quick questions. Get all of them right to unlock the next lesson.
You can take the quiz without an account — logging in just lets your result count toward your progress.