โ† Back to all guides

Elixir TLDR

A rapid reference guide to the Elixir programming language. Everything you need to know, distilled.

v1.19.4 โ€” December 2025

What is Elixir?

Elixir is a dynamic, functional language designed for building scalable and maintainable applications. It runs on the Erlang VM (BEAM), known for running low-latency, distributed, and fault-tolerant systems.

Functional

Immutable data, first-class functions, and pattern matching. Code that's easy to reason about.

Concurrent

Lightweight processes, message passing, and the actor model. Scale to millions of connections.

Fault-Tolerant

Supervisors and "let it crash" philosophy. Build systems that self-heal from failures.

Extensible

Powerful macros and DSLs. Elixir is written in Elixir. Build tools like Phoenix and Ecto.

Basics

Hello World

# hello.exs
IO.puts("Hello, World!")

# Run with: elixir hello.exs

Interactive Shell (IEx)

# Start interactive Elixir
iex

# Start with your project
iex -S mix

# Compile and run
elixir script.exs

Variables

# Variables are immutable (rebinding creates new value)
x = 1
x = 2          # rebinding, not mutation

# Naming conventions
snake_case = "variables and functions"
PascalCase  # modules only
_unused     # underscore prefix for unused vars

# Pin operator - match against existing value
x = 1
^x = 1       # matches (x is still 1)
^x = 2       # raises MatchError

Comments

# Single line comment

# No multi-line comments
# Just use multiple single-line comments

Data Types

Primitive Types

Type Example Notes
Integer 42, 1_000_000, 0xFF Arbitrary precision
Float 3.14, 1.0e-10 64-bit double precision
Boolean true, false Actually atoms
Atom :ok, :error, nil Named constants
String "hello" UTF-8 encoded binary
Charlist 'hello' List of code points

Atoms

# Atoms are constants whose value is their name
:ok
:error
:hello_world

# Module names are atoms
Enum == :"Elixir.Enum"  # true

# nil, true, false are atoms
nil == :nil    # true
true == :true  # true

Strings

# Double-quoted strings (binaries)
name = "world"
"Hello, #{name}!"  # interpolation

# Multi-line strings (heredoc)
"""
Multi-line
string
"""

# String functions
String.length("hello")           # 5
String.upcase("hello")            # "HELLO"
String.split("a,b,c", ",")        # ["a", "b", "c"]
String.replace("hello", "l", "r") # "herro"
"hello" <> " world"              # "hello world"

Collections

# List (linked list)
list = [1, 2, 3]
[0 | list]          # [0, 1, 2, 3] - prepend
[head | tail] = list  # head = 1, tail = [2, 3]

# Tuple (fixed size, contiguous memory)
tuple = {:ok, "value"}
elem(tuple, 1)       # "value"

# Map (key-value store)
map = %{"name" => "Alice", "age" => 30"name"]         # "Alice"

# Map with atom keys (shorthand)
map = %{name: "Alice", age: 30}
map.name             # "Alice"
map[:name]          # "Alice"

# Keyword list (ordered, duplicate keys allowed)
opts = [name: "Alice", age: 30]
opts[:name]          # "Alice"

Range

# Inclusive range
1..5     # 1, 2, 3, 4, 5

# Exclusive range
1...5    # 1, 2, 3, 4 (excludes 5)

# Descending range
5..1//-1  # 5, 4, 3, 2, 1

Sigils

# String
~s(Hello "world")   # "Hello \"world\""
~S(No \n escapes)   # "No \\n escapes"

# Charlist
~c(hello)           # 'hello'

# Word list
~w(foo bar baz)     # ["foo", "bar", "baz"]
~w(foo bar baz)a    # [:foo, :bar, :baz]

# Regex
~r/hello/i          # case-insensitive regex
Regex.match?(~r/foo/, "foobar")  # true

Pattern Matching

The = operator is the match operator, not just assignment. It tries to make the left side match the right side.

Basic Matching

# Simple binding
x = 1                # x = 1
1 = x                # matches! (x is 1)
2 = x                # MatchError!

# Destructuring tuples
{:ok, result} = {:ok, 42# 42

# Destructuring lists
[head | tail] = [1, 2, 3]
head                 # 1
tail                 # [2, 3]

[a, b, c] = [1, 2, 3]
[a, _, c] = [1, 2, 3]  # ignore middle element

Map Matching

# Extract values from map
%{name: name} = %{name: "Alice", age: 30}
name                 # "Alice"

# Match partial structure
%{age: age} = %{name: "Alice", age: 30}
age                  # 30

Pin Operator

x = 1

# Without pin: rebinds x
{x, y} = {2, 3}     # x = 2, y = 3

x = 1
# With pin: matches against x's value
{^x, y} = {1, 3}    # matches! y = 3
{^x, y} = {2, 3}    # MatchError! (x is 1, not 2)

Function Clause Matching

defmodule Math do
  def zero?(0), do: true
  def zero?(_), do: false
end

Math.zero?(0)   # true
Math.zero?(1)   # false
Pattern matching is pervasive in Elixir. Use it in function heads, case expressions, with statements, and receive blocks.

Control Flow

if / unless

# if expression
if condition do
  "truthy"
else
  "falsy"
end

# One-liner
if condition, do: "yes", else: "no"

# unless (negated if)
unless condition, do: "falsy"

case

case value do
  {:ok, result} ->
    "Success: #{result}"
  {:error, reason} ->
    "Error: #{reason}"
  _ ->
    "Unknown"
end

# With guards
case x do
  n when n < 0 -> "negative"
  n when n > 0 -> "positive"
  _ -> "zero"
end

cond

# Multiple conditions (like else-if)
cond do
  x > 10 -> "big"
  x > 5  -> "medium"
  true   -> "small"  # default case
end

with

# Chain pattern matches (happy path)
with {:ok, user} <- fetch_user(id),
     {:ok, posts} <- fetch_posts(user),
     {:ok, comments} <- fetch_comments(posts) do
  # All matched successfully
  {:ok, %{user: user, posts: posts, comments: comments|}
else
  {:error, reason} -> {:error, reason}
  _ -> {:error, :unknown}
end

for (Comprehensions)

# Basic list comprehension
for x <- [1, 2, 3], do: x * 2
# [2, 4, 6]

# With filter
for x <- 1..10, rem(x, 2) == 0, do: x
# [2, 4, 6, 8, 10]

# Multiple generators (nested)
for x <- [1, 2], y <- [:a, :b], do: {x, y}
# [{1, :a}, {1, :b}, {2, :a}, {2, :b}]

# Into different collection
for {k, v} <- %{a: 1, b: 2}, into: %{}, do: {k, v * 2}
# %{a: 2, b: 4}

Functions

Anonymous Functions

# Define with fn
add = fn a, b -> a + b end
add.(1, 2)  # 3 (note the dot)

# Shorthand capture syntax
add = &(&1 + &2)
add.(1, 2)  # 3

# Multiple clauses
handle = fn
  {:ok, result} -> "Success: #{result}"
  {:error, msg} -> "Error: #{msg}"
end

Named Functions

defmodule Math do
  # Public function
  def add(a, b) do
    a + b
  end

  # One-liner
  def multiply(a, b), do: a * b

  # Private function
  defp helper(x), do: x * 2
end

Default Arguments

def greet(name, greeting \\ "Hello") do
  "#{greeting}, #{name}!"
end

greet("Alice")          # "Hello, Alice!"
greet("Alice", "Hi")   # "Hi, Alice!"

Guards

def abs(x) when x >= 0, do: x
def abs(x) when x < 0, do: -x

# Guard expressions
when is_integer(x)
when is_binary(s)      # strings are binaries
when is_list(l)
when is_map(m)
when is_atom(a)
when is_nil(x)
when x in [1, 2, 3]
when x > 0 and x < 10

Pipe Operator

# Transform data through a pipeline
result =
  data
  |> parse()
  |> Enum.filter(&valid?/1)
  |> Enum.map(&transform/1)
  |> Enum.sort()

# Pipes pass result as first argument
"hello"
|> String.upcase()
|> String.reverse()
# "OLLEH"

Function Capturing

# Capture named function
Enum.map([1, 2, 3], &Math.double/1)

# Capture with arity
add = &Kernel.+/2
add.(1, 2)  # 3

# Create partial application
add_one = &(&1 + 1)
add_one.(5)  # 6

Modules & Structs

Defining Modules

defmodule MyApp.User do
  # Module attribute (compile-time constant)
  @default_role :member

  def new(name) do
    %{name: name, role: @default_role}
  end
end

Structs

defmodule User do
  # Define struct with defaults
  defstruct [:name, :email, age: 0, active: true]

  # Enforce required keys
  @enforce_keys [:name, :email]
end

# Create struct
user = %User{name: "Alice", email: "alice@example.com"}

# Access fields
user.name         # "Alice"

# Update struct
%{user | age: 30# Pattern match on struct
%User{name: name} = user

Protocols

# Define a protocol (interface)
defprotocol Displayable do
  def display(data)
end

# Implement for a type
defimpl Displayable, for: User do
  def display(user) do
    "User: #{user.name}"
  end
end

# Use protocol
Displayable.display(user)

Behaviours

# Define a behaviour (interface for modules)
defmodule Parser do
  @callback parse(String.t()) :: {:ok, term()} | {:error, String.t()}
end

# Implement behaviour
defmodule JSONParser do
  @behaviour Parser

  @impl true
  def parse(json) do
    # implementation
  end
end

Module Directives

defmodule MyModule do
  # Import functions from another module
  import Enum, only: [map: 2, filter: 2]

  # Alias a module
  alias MyApp.Accounts.User
  alias MyApp.Accounts.User, as: U

  # Require for macros
  require Logger

  # Use (invoke __using__ macro)
  use GenServer
end

Enum & Stream

The Enum module provides functions for working with collections. Stream provides lazy equivalents.

Common Enum Functions

list = [1, 2, 3, 4, 5]

# Transform
Enum.map(list, &(&1 * 2))           # [2, 4, 6, 8, 10]
Enum.filter(list, &(&1 > 2))        # [3, 4, 5]
Enum.reject(list, &(&1 > 2))        # [1, 2]

# Reduce
Enum.reduce(list, 0, &(&1 + &2))    # 15 (sum)
Enum.sum(list)                      # 15
Enum.product(list)                  # 120

# Search
Enum.find(list, &(&1 > 3))         # 4
Enum.any?(list, &(&1 > 4))         # true
Enum.all?(list, &(&1 > 0))         # true
Enum.member?(list, 3)              # true

# Access
Enum.at(list, 2)                   # 3
Enum.take(list, 3)                 # [1, 2, 3]
Enum.drop(list, 2)                 # [3, 4, 5]
Enum.first(list)                   # 1
Enum.last(list)                    # 5

# Sort & Organize
Enum.sort([3, 1, 2])               # [1, 2, 3]
Enum.sort_by(users, & &1.age)      # sort by field
Enum.reverse(list)                 # [5, 4, 3, 2, 1]
Enum.uniq([1, 1, 2, 2])            # [1, 2]
Enum.group_by(users, & &1.role)   # group by field

# Join
Enum.join(["a", "b"], ",")         # "a,b"
Enum.zip([1, 2], [:a, :b])         # [{1, :a}, {2, :b}]

Stream (Lazy Evaluation)

# Streams are lazy - no computation until needed
stream = 1..1_000_000
         |> Stream.map(&(&1 * 2))
         |> Stream.filter(&(rem(&1, 2) == 0))
         |> Stream.take(10)

# Force evaluation
Enum.to_list(stream)  # [4, 8, 12, 16, 20, 24, 28, 32, 36, 40]

# Infinite streams
Stream.iterate(0, &(&1 + 1))  # 0, 1, 2, 3, ...
Stream.cycle([1, 2, 3])       # 1, 2, 3, 1, 2, 3, ...
Stream.repeatedly(&:rand.uniform/0)

Map Operations

map = %{a: 1, b: 2, c: 3Map.get(map, :a)                   # 1
Map.get(map, :z, :default)        # :default
Map.put(map, :d, 4)                # %{a: 1, b: 2, c: 3, d: 4}
Map.delete(map, :a)                # %{b: 2, c: 3}
Map.keys(map)                      # [:a, :b, :c]
Map.values(map)                    # [1, 2, 3]
Map.merge(map, %{d: 4})           # %{a: 1, b: 2, c: 3, d: 4}
Map.update!(map, :a, &(&1 + 10))  # %{a: 11, b: 2, c: 3}

Processes

Elixir processes are lightweight (not OS processes). They're isolated and communicate via message passing.

Spawning Processes

# Spawn a process
pid = spawn(fn ->
  IO.puts("Hello from process!")
end)

# Check if alive
Process.alive?(pid)

# Get current process
self()  # returns PID

# Spawn with link (crash together)
spawn_link(fn -> raise "oops" end)

Message Passing

# Send message
send(pid, {:hello, "world"})

# Receive messages
receive do
  {:hello, msg} ->
    IO.puts("Got: #{msg}")
  _ ->
    IO.puts("Unknown message")
after
  5000 -> IO.puts("Timeout!")
end

Simple Process Example

defmodule Counter do
  def start(initial \\ 0) do
    spawn(fn -> loop(initial) end)
  end

  defp loop(count) do
    receive do
      :increment ->
        loop(count + 1)
      {:get, caller} ->
        send(caller, {:count, count})
        loop(count)
    end
  end
end

# Usage
pid = Counter.start()
send(pid, :increment)
send(pid, {:get, self()})
receive do
  {:count, n} -> IO.puts("Count: #{n}")
end

Tasks

# Async task
task = Task.async(fn -> expensive_operation() end)
result = Task.await(task)  # blocks until done

# Multiple concurrent tasks
tasks = Enum.map(urls, fn url ->
  Task.async(fn -> fetch(url) end)
end)
results = Task.await_many(tasks)

Agents (Simple State)

# Start an agent with initial state
{:ok, agent} = Agent.start_link(fn -> [] end)

# Get state
Agent.get(agent, fn list -> list end)

# Update state
Agent.update(agent, fn list -> [1 | list] end)

# Get and update
Agent.get_and_update(agent, fn list ->
  {length(list), [2 | list]}
end)

GenServer

GenServer is a behaviour for implementing server processes. It handles sync/async calls, state management, and supervision.

Basic GenServer

defmodule Counter do
  use GenServer

  # Client API
  def start_link(initial \\ 0) do
    GenServer.start_link(__MODULE__, initial, name: __MODULE__)
  end

  def increment() do
    GenServer.cast(__MODULE__, :increment)
  end

  def get() do
    GenServer.call(__MODULE__, :get)
  end

  # Server Callbacks
  @impl true
  def init(initial) do
    {:ok, initial}
  end

  @impl true
  def handle_cast(:increment, count) do
    {:noreply, count + 1}
  end

  @impl true
  def handle_call(:get, _from, count) do
    {:reply, count, count}
  end
end

GenServer Callbacks

Callback Triggered By Returns
init/1 start_link/3 {:ok, state}
handle_call/3 GenServer.call/2 {:reply, response, state}
handle_cast/2 GenServer.cast/2 {:noreply, state}
handle_info/2 Any other message {:noreply, state}
terminate/2 Process shutdown :ok

Call vs Cast

# call - synchronous, blocks until reply
result = GenServer.call(pid, :some_request)

# cast - asynchronous, fire and forget
GenServer.cast(pid, :some_request)
Use call when you need a response. Use cast for fire-and-forget operations where you don't need confirmation.

OTP & Supervisors

OTP (Open Telecom Platform) provides behaviours for building fault-tolerant applications. Supervisors monitor and restart failed processes.

Supervisor

defmodule MyApp.Supervisor do
  use Supervisor

  def start_link(opts) do
    Supervisor.start_link(__MODULE__, :ok, opts)
  end

  @impl true
  def init(:ok) do
    children = [
      # Child specifications
      {Counter, 0},
      {Cache, []},
      %{
        id: MyWorker,
        start: {MyWorker, :start_link, [[]]|}
      }
    ]

    Supervisor.init(children, strategy: :one_for_one)
  end
end

Supervision Strategies

:one_for_one
Only the crashed child is restarted. Other children are unaffected.
:one_for_all
All children are restarted when one crashes. Use when children are interdependent.
:rest_for_one
Crashed child and all children started after it are restarted.

Application

defmodule MyApp.Application do
  use Application

  @impl true
  def start(_type, _args) do
    children = [
      MyApp.Supervisor,
      {Phoenix.PubSub, name: MyApp.PubSub}
    ]

    opts = [strategy: :one_for_one, name: MyApp.Supervisor]
    Supervisor.start_link(children, opts)
  end
end
The "let it crash" philosophy means you don't defensively handle every error. Let processes crash and let supervisors restart them in a known good state.

Mix Build Tool

Project Commands

# Create new project
mix new my_app
mix new my_app --sup  # with supervision tree

# Dependencies
mix deps.get          # fetch dependencies
mix deps.update --all # update all deps

# Compile
mix compile

# Run
mix run               # run project
mix run --no-halt     # keep running
iex -S mix            # interactive with project

# Test
mix test
mix test test/my_test.exs  # specific file
mix test --cover           # with coverage

# Format code
mix format

mix.exs

defmodule MyApp.MixProject do
  use Mix.Project

  def project do
    [
      app: :my_app,
      version: "0.1.0",
      elixir: "~> 1.19",
      deps: deps()
    ]
  end

  def application do
    [
      extra_applications: [:logger],
      mod: {MyApp.Application, []}
    ]
  end

  defp deps do
    [
      {:phoenix, "~> 1.7"},
      {:ecto_sql, "~> 3.10"},
      {:jason, "~> 1.4"}
    ]
  end
end

Testing

# test/my_app_test.exs
defmodule MyAppTest do
  use ExUnit.Case
  doctest MyApp

  describe "add/2" do
    test "adds two numbers" do
      assert MyApp.add(1, 2) == 3
    end

    test "handles negative numbers" do
      assert MyApp.add(-1, 1) == 0
    end
  end

  test "raises on invalid input" do
    assert_raise ArgumentError, fn ->
      MyApp.parse!(:invalid)
    end
  end
end

Common Assertions

assert 1 + 1 == 2
refute 1 + 1 == 3
assert_raise RuntimeError, fn -> raise "boom" end
assert_receive {:msg, _}, 1000  # wait up to 1s
assert result =~ "pattern"       # regex/string match