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.
Project — mix 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.
REPL — iex 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 flow — if/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
endPipe 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].
Collections — Enum (eager) and Stream (lazy) work uniformly across lists, maps, ranges, and any Enumerable. Map access: map[:key] or map.key for atom keys.
Intermediate
Modules — defmodule, 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 primitives — spawn/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/O — File, 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 highlights — Enum, 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.
Reflection — Code.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
endMacro.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 callbacks — use 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 behaviors — GenServer, 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 withmix format --check-formatted. - Linter: Credo (community standard); Dialyzer (
mix dialyzerviadialyxir) for static analysis. - Idiomatic: pattern match in function heads instead of
if; usewithfor happy-path chains; return{:ok, x}/{:error, reason}rather thannil/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 — neverString.to_atom/1on user input; useString.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/1on 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===distinguishes1(int) from1.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
:infinityor higher. - Hot upgrades are hard in practice; most teams use rolling deploys.
withclause failure returns the unmatched value as-is — easy to lose error context without anelsebranch.- 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_nifor dirty schedulers.
Citations
- Elixir docs index: https://elixir-lang.org/docs.html
- HexDocs (API ref): https://hexdocs.pm/elixir/
- Getting Started: https://hexdocs.pm/elixir/introduction.html
- Release notes 1.19: https://github.com/elixir-lang/elixir/releases/tag/v1.19.5
- Set-theoretic types intro: https://elixir-lang.org/blog/2024/06/12/elixir-v1-17-0-released/
- OTP design principles: https://www.erlang.org/doc/system/design_principles.html
- BEAM book (community): https://blog.stenmans.org/theBeamBook/
- Phoenix: https://www.phoenixframework.org/
- Nerves: https://nerves-project.org/
- Hex package manager: https://hex.pm/
- Mnesia caveats: https://hexdocs.pm/mnesia/