Elixir — Reference

Source: https://elixir-lang.org/docs.html

Elixir

  • Created: 2011 by Jose Valim
  • Latest stable: 1.19.5 (October 16, 2025), supports Erlang/OTP 26, 27, 28
  • Paradigms: Functional, concurrent, distributed, fault-tolerant, dynamic
  • Typing: Dynamically typed, strong; gradual set-theoretic type system being introduced incrementally since 1.17
  • Memory: Per-process heaps on the BEAM, generational GC; immutable data structures
  • Compilation: Compiled to BEAM bytecode, executes on the Erlang VM (BEAM); supports hot code reloading
  • Primary domains: Distributed systems, web (Phoenix/LiveView), telecoms, embedded (Nerves), data pipelines (Broadway/Flow), real-time messaging
  • Official docs: https://hexdocs.pm/elixir/ and https://elixir-lang.org/docs.html

At a glance

Elixir is a dynamic, functional language built on the Erlang VM (BEAM). It inherits Erlang/OTP actor-model concurrency, supervision trees, hot upgrades, and distributed messaging while adding modern syntax, macros, polymorphism via protocols, the Mix build tool, and the ExUnit test framework. Six bundled applications ship in every install: elixir, eex, ex_unit, iex, logger, and mix.

Getting started

Install — Use asdf or the official installers from elixir-lang.org. On macOS: brew install elixir. Verify with elixir --version. Requires Erlang/OTP 26+ for Elixir 1.19.

Hello world (script hello.exs):

IO.puts("Hello, world!")

Run with elixir hello.exs.

Projectmix new my_app scaffolds:

my_app/
  mix.exs        # build config and deps
  lib/my_app.ex  # source
  test/          # ExUnit tests
  config/        # runtime config

mix deps.get fetches Hex packages, mix test runs tests, iex -S mix boots a REPL with the project loaded.

REPLiex is the interactive shell. h Enum.map shows docs, i value inspects, recompile() reloads.

Basics

Types/literals — Integers (arbitrary precision), floats (64-bit), booleans (true/false/nil), atoms (:ok, :error), strings (UTF-8 binaries "hi"), charlists (~c"hi"), tuples ({:ok, 1}), lists ([1,2,3]), maps (%{a: 1}), keyword lists ([a: 1, b: 2]), structs (%User{}), binaries (<<1, 2, 3>>), functions, PIDs, references.

Variables — Lower-case, immutable rebinding allowed: x = 1; x = 2. Pattern matching is the primary binding form: {:ok, n} = {:ok, 42}. ^x pins a value in a match.

Control flowif/else, unless, cond, case, with (railway-oriented chains), try/rescue/catch. Pattern matching pervades every form.

Functions — Anonymous: fn x -> x * 2 end, called with .: f.(3). Named functions live in modules:

defmodule Math do
  def double(x), do: x * 2
  def add(a, b), do: a + b
end

Pipe operator: [1,2,3] |> Enum.map(&(&1 * 2)) |> Enum.sum().

Strings — Binaries, UTF-8 by default. Interpolation: "hello #{name}". String module for ops, <> to concat. Sigils: ~r/regex/, ~s"str", ~D[2025-01-01].

CollectionsEnum (eager) and Stream (lazy) work uniformly across lists, maps, ranges, and any Enumerable. Map access: map[:key] or map.key for atom keys.

Intermediate

Modulesdefmodule, import, alias, require, use. Module attributes: @moduledoc, @doc, @spec, @type (for Dialyzer), @behaviour.

Type system — Static analyzer is Dialyzer (success typing). Elixir 1.17+ ships an incremental set-theoretic type system that infers types for module signatures and flags dead clauses. Typespecs (@spec add(integer, integer) :: integer) feed both. See https://elixir-lang.org/blog/2024/06/12/elixir-v1-17-0-released/.

Error handling — Two flavors: error tuples ({:ok, x} / {:error, reason}) for expected failures (idiomatic), and exceptions (raise, try/rescue) for truly exceptional cases. The “let it crash” philosophy says: do not defensively code; let supervisors restart misbehaving processes.

Concurrency primitivesspawn/1 creates a process (BEAM-level, not OS), send/2 and receive exchange messages. Higher-level: Task.async/await, Agent (state holder), GenServer (server behavior with call/cast), Supervisor (restart strategies: :one_for_one, :one_for_all, :rest_for_one), DynamicSupervisor, Registry, PartitionSupervisor.

I/OFile, IO, Path, Port (for OS process I/O), :gen_tcp/:gen_udp from Erlang stdlib, :ssl. IO.inspect/2 is the universal print-and-return debug tool.

Stdlib highlightsEnum, Stream, Map, MapSet, Keyword, String, Regex, DateTime, Calendar, URI, Base, Process, Node, Application, Code, Macro, :ets, :dets, :mnesia, :crypto, :digraph.

Advanced

Memory/GC — Each BEAM process has its own private heap; GC is per-process and generational, so a single process collection never stops the world for the system. Large binaries (>64 bytes) live on a shared refcounted heap. Process mailboxes are unbounded; backpressure is the application responsibility.

Concurrency deep dive — BEAM schedulers (one per core) preemptively multiplex millions of lightweight processes. Reductions count function calls; preemption fires every ~2000 reductions. NIFs that do not yield can starve schedulers (use dirty NIFs or enif_consume_timeslice). Distribution: Node.connect/1 joins a cluster; messages cross machines transparently. Failure detection via net ticks. Tools: :observer.start(), :recon, :erts_debug.size/1.

FFI — Three flavors: NIFs (Native Implemented Functions, C/Rust via Rustler — fast but can crash the VM), Ports (OS-process IPC, safe but slow), Port Drivers (loadable C drivers, intermediate). :erlang.open_port/2 for ports.

ReflectionCode.fetch_docs/1, Module.definitions_in/1, __info__/1 callback on every module, Mix.Project.config(). AST inspection via quote/unquote and Macro.to_string/1.

Performance tools:fprof, :eprof, :cprof, :observer, Benchee (de facto microbench library), :recon for production diagnostics, mix profile.cprof, ExProf. ETS for shared in-memory tables (lock-free reads).

God mode

Macros — Elixir is homoiconic: code is AST. quote do ... end reifies code as a 3-tuple {op, meta, args}. unquote splices values in:

defmacro unless(cond, do: body) do
  quote do
    if !unquote(cond), do: unquote(body)
  end
end

Macro.expand/2, Macro.to_string/1, Macro.prewalk/2 traverse AST. defmacrop for private macros. Hygiene: variables introduced by quote do not leak; var! overrides hygiene.

use and compile callbacksuse M invokes M.__using__/1 at the call site, classic for DSLs (Phoenix use Phoenix.Controller). @before_compile and @after_compile hooks let modules inspect or finalize themselves.

BEAM internals — Compiled .beam files are chunked binaries (Atom, Code, ImpT, ExpT, LocT, etc.). :beam_lib.chunks/2 extracts. :erts_debug.df/1 disassembles. Disassemble Elixir bytecode: Mix.Tasks.Compile.Erlang and :beam_disasm.

OTP behaviorsGenServer, GenStateMachine, Supervisor, Application, :gen_event. Custom behaviors via @callback and @behaviour.

Hot code reloading — Two versions of a module can coexist (current and old); :code.purge/1 evicts old. Releases via mix release build self-contained tarballs; Distillery-style appup files describe upgrade migrations (most prod systems instead use blue-green rolling deploys).

ETS/DETS/Mnesia — ETS is in-memory key-value (set, ordered_set, bag, duplicate_bag); reads can be lock-free via :read_concurrency. DETS is the on-disk sibling. Mnesia is a distributed transactional DB (used carefully — has surprising failure modes, see https://hexdocs.pm/mnesia/).

Distributed Erlang:net_kernel.start/1, EPMD broker on port 4369, cookie-based auth (NOT cryptographic — wrap in TLS via inet_dist_use_interface). :global registry, :pg process groups.

Gleam interop — Gleam targets BEAM bytecode and can call Elixir/Erlang modules directly via @external(erlang, "module", "fun"); Elixir can call Gleam-compiled modules transparently.

Idioms & style

  • Naming: snake_case for functions/variables, CamelCase for modules, ALL_CAPS for constants in attributes.
  • Formatter: mix format (built-in, opinionated, non-configurable beyond locals/.formatter.exs). Run in CI with mix format --check-formatted.
  • Linter: Credo (community standard); Dialyzer (mix dialyzer via dialyxir) for static analysis.
  • Idiomatic: pattern match in function heads instead of if; use with for happy-path chains; return {:ok, x} / {:error, reason} rather than nil/raise; pipe transformations; let supervisors handle failure.
  • Expert review focus: missing supervisor strategy, unbounded mailboxes, blocking calls inside GenServer.handle_call, large binaries leaking refcounts, ETS table ownership on process death, atom exhaustion (atoms are not GC’d — never String.to_atom/1 on user input; use String.to_existing_atom/1).

Ecosystem

  • Web: Phoenix (full-stack), Phoenix LiveView (server-rendered reactive UI), Plug (middleware), Bandit/Cowboy (HTTP servers).
  • Data: Ecto (DB toolkit + query DSL), Broadway (data ingestion pipelines), Flow (parallel collections), GenStage (back-pressured pipelines).
  • Embedded/IoT: Nerves (Linux firmware platform).
  • ML: Nx (numerical computing, like NumPy), Axon (neural nets), Bumblebee (pretrained models), Explorer (dataframes), Livebook (notebooks).
  • Testing: ExUnit (built-in), StreamData (property-based), Mox (mocks), Wallaby (browser tests).
  • Docs: ExDoc generates HTML from @moduledoc/@doc; published on https://hexdocs.pm.
  • Notable users: Discord (millions of concurrent users), Pinterest, Bleacher Report, Heroku Routing, PepsiCo, Cars.com, Change.org, Brex.

Gotchas

  • Atoms are not GC’d — limit ~1M; String.to_atom/1 on untrusted input is a DoS.
  • Charlists vs strings'hi' is [104, 105] (a list of codepoints), "hi" is a binary. Erlang APIs often expect charlists.
  • Default arguments with multiple clauses require a separate header: def foo(x \\ 1) then clauses.
  • Floats use == between values, but === distinguishes 1 (int) from 1.0 (float).
  • Enum.into(map) overwrites — duplicate keys silently win.
  • Mailbox selective receive is O(n) on mailbox size; long mailboxes cause subtle slowdowns.
  • GenServer.call timeout defaults to 5s; long calls need explicit :infinity or higher.
  • Hot upgrades are hard in practice; most teams use rolling deploys.
  • with clause failure returns the unmatched value as-is — easy to lose error context without an else branch.
  • Distributed Erlang cookies are plaintext and trivially sniffable; never run distribution over untrusted networks without TLS.
  • NIFs that block wreck the scheduler; use enif_schedule_nif or dirty schedulers.

Citations