# Common Mistakes What "bad Elixir" looks like, based on what the source code explicitly warns against or demonstrates the correct way to avoid. ## 1. Using `++` in a Loop (O(n²) List Building) **Source:** `lib/elixir/lib/enum.ex:306-320` (internal macros all use prepend) ```elixir # What the source does: prepend then reverse defmacrop next(_, entry, acc) do quote(do: [unquote(entry) | unquote(acc)]) end ``` **The mistake:** ```elixir # O(n²) — copies the entire left list for every element Enum.reduce(items, [], fn item, acc -> acc ++ [transform(item)] end) ``` **The fix:** ```elixir # O(n) — prepend is O(1), reverse once at the end items |> Enum.map(&transform/1) # or Enum.reduce(items, [], fn item, acc -> [transform(item) | acc] end) |> Enum.reverse() ``` --- ## 2. Forgetting `@impl true` **Source:** `lib/elixir/lib/gen_server.ex:44-56` (every callback uses @impl) **The mistake:** ```elixir defmodule MyServer do use GenServer # Typo! This will never be called — no warning without @impl def handle_cll(msg, _from, state) do {:reply, msg, state} end end ``` **The fix:** ```elixir @impl true def handle_call(msg, _from, state) do {:reply, msg, state} end ``` With `@impl true`, the compiler catches the typo at compile time. --- ## 3. Not Handling All `with` Failure Cases **Source:** `lib/elixir/lib/kernel/special_forms.ex:1680-1715` (with Beware! section) **The mistake:** ```elixir with {:ok, width} <- Map.fetch(opts, "width"), {:ok, height} <- Map.fetch(opts, "height") do {:ok, width * height} else # Only handles one case — what if Map.fetch returns something else? :error -> {:error, :missing_field} end ``` If an `else` block is used and no clause matches, a `WithClauseError` is raised. **The fix:** Either handle all possible non-match values in `else`, or better yet, normalize return values in helper functions so you don't need `else` at all. --- ## 4. async Without await **Source:** `lib/elixir/lib/task.ex:38-40` > If you are using async tasks, you **must await** a reply as they are *always* sent. **The mistake:** ```elixir # Leaked reference — message sits in mailbox forever Task.async(fn -> send_email(user) end) # Never awaited! ``` **The fix:** ```elixir # Fire-and-forget: use start_child Task.Supervisor.start_child(MyApp.TaskSupervisor, fn -> send_email(user) end) # OR if you need the result: task = Task.async(fn -> send_email(user) end) Task.await(task) ``` --- ## 5. Anonymous Functions in Distributed Agents **Source:** `lib/elixir/lib/agent.ex:141-160` ("A word on distributed agents") > In a distributed setup with multiple nodes, the API that accepts anonymous > functions only works if the caller (client) and the agent have the same > version of the caller module. **The mistake:** ```elixir # Fails if nodes have different code versions Agent.get({MyAgent, :remote@node}, fn state -> state.count end) ``` **The fix:** ```elixir # Use MFA (module, function, args) for distributed calls Agent.get({MyAgent, :remote@node}, MyModule, :get_count, []) ``` --- ## 6. Starting Processes Outside Supervision Trees **Source:** `lib/elixir/lib/task.ex:100-115` > We encourage developers to rely on supervised tasks as much as possible. **The mistake:** ```elixir # No supervision, no monitoring, no logging spawn(fn -> do_important_work() end) # Or: Task.async(fn -> do_important_work() end) |> Task.await() # Linked to caller but not supervised ``` **The fix:** ```elixir # Add to your supervision tree: {Task.Supervisor, name: MyApp.TaskSupervisor} # Then use it: Task.Supervisor.start_child(MyApp.TaskSupervisor, fn -> do_important_work() end) ``` --- ## 7. Putting State Logic in Controllers **Source:** `lib/phoenix/controller.ex:28-45` Controllers are shown as thin dispatch layers: ```elixir def show(conn, %{"id" => id}) do user = Repo.get(User, id) render(conn, :show, user: user) end ``` **The mistake:** ```elixir def create(conn, params) do # Business logic in the controller changeset = User.changeset(%User{}, params) if changeset.valid? do user = Repo.insert!(changeset) send_welcome_email(user) update_analytics(user) notify_admin(user) render(conn, :show, user: user) else render(conn, :error, errors: changeset.errors) end end ``` **The fix:** Move business logic to a context module. The controller just dispatches: ```elixir def create(conn, params) do case Accounts.register_user(params) do {:ok, user} -> render(conn, :show, user: user) {:error, changeset} -> render(conn, :error, errors: changeset.errors) end end ``` --- ## 8. Using `:permanent` Restart for One-Shot Tasks **Source:** `lib/elixir/lib/task.ex:178-186` > a Task has a default `:restart` of `:temporary`. This means the task will > not be restarted even if it crashes. **The mistake:** ```elixir # Will restart infinitely if the HTTP call keeps failing use Task, restart: :permanent def start_link(url) do Task.start_link(fn -> HTTP.get!(url) end) end ``` **The fix:** Use `:temporary` (default) for one-shot work. Use `:transient` if you want restart only on abnormal exit: ```elixir use Task, restart: :transient ``` --- ## 9. Pattern Matching the Internals of Opaque Types **Source:** `lib/elixir/lib/task.ex:298-300` ```elixir @opaque ref :: reference() ``` **The mistake:** ```elixir # Accessing internal structure of an opaque type %Task{ref: ref} = task send(ref, :custom_message) # This breaks if internals change ``` **The fix:** Use the public API. If a type is `@opaque`, its structure is not guaranteed between versions. Use functions like `Task.await/2` that work with the type properly. --- ## 10. Not Using `on_exit` for Test Cleanup **Source:** `lib/ex_unit/lib/ex_unit/case.ex:86-94` **The mistake:** ```elixir test "writes to file" do File.write!("/tmp/test_file", "data") assert File.read!("/tmp/test_file") == "data" File.rm!("/tmp/test_file") # Never runs if assert above fails! end ``` **The fix:** ```elixir setup do path = "/tmp/test_file_#{System.unique_integer()}" on_exit(fn -> File.rm(path) end) {:ok, path: path} end test "writes to file", %{path: path} do File.write!(path, "data") assert File.read!(path) == "data" # Cleanup happens automatically, even on failure end ```