Coding · Programming languages
Elixir tutorial for beginners
Short answer
Elixir is a dynamic, functional language that runs on the Erlang virtual machine (BEAM), designed for scalable, fault-tolerant and concurrent systems. Its core ideas are immutable data, pattern matching, the pipe operator and lightweight processes that communicate by messages. This page covers setup, types, pattern matching, control flow, modules and functions, collections, processes and error handling.
Open Regex testerWhat Elixir is and what it is used for
Elixir is a dynamic, functional language created by José Valim and first released in 2012. It runs on the BEAM, the virtual machine of Erlang, so it inherits Erlang's lightweight processes, fault tolerance and support for distributed systems, while adding a friendly syntax, macros and modern tooling.
Elixir is used for web applications and APIs with the Phoenix framework, real-time features such as chat and live dashboards (Phoenix LiveView), messaging systems, data pipelines, embedded devices with Nerves, and machine learning with Nx. Companies choose it for services that must handle many concurrent connections and keep running when parts fail.
Install Elixir and run Hello, world
Install Elixir from elixir-lang.org (it also installs or requires Erlang/OTP). Scripts use the .exs extension; compiled project files use .ex. iex starts the interactive shell.
# hello.exs
IO.puts("Hello, world!")elixir hello.exs
# Hello, world!
iex # interactive shell; type h(Enum.map) for help
mix new my_app # create a new project with Mix, the build toolComments start with #. Parentheses around function arguments are optional in many places, so IO.puts "hi" also works.
Basic types and immutability
All data in Elixir is immutable. Rebinding a name with = points it to a new value; it never changes the old one. Strings are UTF-8 binaries written in double quotes, and atoms are named constants that start with a colon.
name = "Elixir"
year = 2012
IO.puts("#{name} since #{year}") # Elixir since 2012
IO.puts(10 / 4) # 2.5 (always a float)
IO.puts(div(10, 4)) # 2
IO.puts(rem(10, 4)) # 2
IO.puts("ab" <> "cd") # abcd (string concatenation)
IO.puts(String.length("héllo")) # 5 (counts characters, not bytes)
IO.inspect(:ok) # :ok
IO.inspect(is_atom(true)) # true (booleans are atoms)| Type | Example | Notes |
|---|---|---|
| integer | 42, 1_000_000 | arbitrary precision |
| float | 3.14 | 64-bit double precision |
| atom | :ok, :error | constants; true, false and nil are atoms |
| string | "hello" | UTF-8 binary; #{} interpolation |
| list | [1, 2, 3] | linked list |
| tuple | {:ok, 1} | fixed size, stored contiguously |
| map | %{"a" => 1} | key-value store |
Pattern matching
In Elixir, = is the match operator. The left side is a pattern; if the right side fits it, variables in the pattern are bound. Pattern matching is used everywhere: in assignments, case, function heads and error handling.
{:ok, value} = {:ok, 42}
IO.puts(value) # 42
[first | rest] = [1, 2, 3]
IO.inspect({first, rest}) # {1, [2, 3]}
%{name: n} = %{name: "Ann", age: 31}
IO.puts(n) # Ann
result = {:error, :not_found}
message =
case result do
{:ok, v} -> "got #{v}"
{:error, reason} -> "failed: #{reason}"
end
IO.puts(message) # failed: not_found
x = 1
^x = 1 # pin operator: match the current valueIf a pattern does not match, Elixir raises a MatchError. The pin operator ^ means "use the existing value of this variable" instead of rebinding it.
Control flow
Elixir has case, cond and if/unless, and all of them return values. There are no for/while loops that mutate counters; you use recursion, the Enum module or the for comprehension instead.
temp = 22
feel =
cond do
temp > 30 -> "hot"
temp >= 15 -> "mild"
true -> "cold"
end
IO.puts(feel) # mild
IO.puts(if(rem(temp, 2) == 0, do: "even", else: "odd")) # even
squares = for n <- 1..5, rem(n, 2) == 1, do: n * n
IO.inspect(squares) # [1, 9, 25]Only false and nil are falsy; everything else, including 0 and "", is truthy.
Functions, modules and the pipe operator
Named functions live in modules defined with defmodule. A function can have several clauses, chosen by pattern matching and guards (when). Anonymous functions are written fn ... end and called with a dot. The pipe operator |> passes the result of one expression as the first argument of the next call.
defmodule Math do
def factorial(0), do: 1
def factorial(n) when n > 0, do: n * factorial(n - 1)
def sign(n) when n < 0, do: :negative
def sign(0), do: :zero
def sign(_n), do: :positive
end
IO.puts(Math.factorial(5)) # 120
IO.inspect(Math.sign(-3)) # :negative
double = fn x -> x * 2 end
IO.puts(double.(21)) # 42 (note the dot)
" Elixir is fun "
|> String.trim()
|> String.upcase()
|> String.split()
|> IO.inspect() # ["ELIXIR", "IS", "FUN"]Functions are identified by name and arity (number of arguments), written as String.split/1 or Math.factorial/1 in documentation.
Collections: lists, maps, keyword lists and Enum
The Enum module works with any enumerable collection and provides map, filter, reduce, sort_by, group_by and many more. Maps are the main key-value structure; keyword lists, such as [size: 10, color: :red], are commonly used for options.
nums = [5, 3, 8, 1]
IO.inspect(Enum.sort(nums)) # [1, 3, 5, 8]
IO.inspect(Enum.map(nums, &(&1 * 10))) # [50, 30, 80, 10]
IO.inspect(Enum.filter(nums, fn n -> n > 2 end)) # [5, 3, 8]
IO.puts(Enum.reduce(nums, 0, &+/2)) # 17
user = %{name: "Ann", age: 31}
older = %{user | age: 32} # update an existing key
IO.puts(user.age) # 31 (unchanged)
IO.puts(older.age) # 32
with_city = Map.put(user, :city, "Oslo")
IO.puts(with_city.city) # Oslo
IO.inspect(Map.get(user, :email, "none")) # "none"The capture operator & creates short anonymous functions: &(&1 * 10) means fn x -> x * 10 end, and &+/2 refers to the + function with two arguments.
Processes and concurrency
Elixir's signature feature is the BEAM process: a very lightweight, isolated unit of execution with its own memory. Processes communicate only by sending messages, and a single machine can run hundreds of thousands of them. Supervisors restart processes that crash, which is the basis of Elixir's "let it crash" approach to fault tolerance.
parent = self()
spawn(fn ->
send(parent, {:result, 6 * 7})
end)
receive do
{:result, value} -> IO.puts("Got #{value}") # Got 42
after
1_000 -> IO.puts("timeout")
end
tasks = for n <- 1..3, do: Task.async(fn -> n * n end)
IO.inspect(Task.await_many(tasks)) # [1, 4, 9]In real applications you rarely call spawn directly. You use abstractions from OTP such as Task, GenServer, Agent and Supervisor.
Error handling
Idiomatic Elixir returns tagged tuples such as {:ok, value} or {:error, reason} for expected failures and uses case or with to handle them. Functions whose names end in !, such as File.read!/1, raise an exception instead. Exceptions can be caught with try/rescue, but this is uncommon.
case File.read("missing.txt") do
{:ok, text} -> IO.puts(text)
{:error, reason} -> IO.puts("Cannot read: #{reason}") # Cannot read: enoent
end
with {a, ""} <- Integer.parse("40"),
{b, ""} <- Integer.parse("2") do
IO.puts(a + b) # 42
else
_ -> IO.puts("not a whole number")
end
try do
String.to_integer("abc")
rescue
e in ArgumentError -> IO.puts("Error: #{Exception.message(e)}")
endwith chains several matches and stops at the first one that does not match, returning that value. It avoids deeply nested case expressions.
A small complete program: word frequency
This script counts words in a text and prints the three most common ones. It is a typical Elixir pipeline built from String and Enum functions.
defmodule WordFreq do
def top(text, n) do
text
|> String.downcase()
|> String.split(~r/[^a-z']+/, trim: true)
|> Enum.frequencies()
|> Enum.sort_by(fn {word, count} -> {-count, word} end)
|> Enum.take(n)
end
end
"The cat and the dog. The dog sat, and the cat ran."
|> WordFreq.top(3)
|> Enum.each(fn {word, count} -> IO.puts("#{word}: #{count}") end)
# the: 4
# and: 2
# cat: 2Where to go next
- The official Getting Started guide on hexdocs.pm and the elixir-lang.org website.
- HexDocs, where the documentation of Elixir and every Hex package is published.
- The Phoenix framework guides for web development.
- Elixir School, a free community curriculum, and Exercism's Elixir track for practice.
FAQ
What is the difference between Elixir and Erlang?
Both run on the same BEAM virtual machine and can call each other's code. Elixir adds a Ruby-inspired syntax, macros, the pipe operator and tools such as Mix and Hex, while Erlang is the older language that OTP was built in.
Is Elixir good for web development?
Yes. The Phoenix framework is fast and handles many simultaneous connections, and LiveView lets you build interactive pages with server-rendered HTML and little JavaScript.
Is Elixir statically typed?
Elixir is dynamically typed. Recent versions are adding a gradual set-theoretic type system to the compiler, and many teams also use Dialyzer with typespecs to find type errors.
What does |> mean in Elixir?
|> is the pipe operator. x |> f(y) is the same as f(x, y), so a chain of transformations can be read from top to bottom.
What are .ex and .exs files?
.ex files are compiled as part of a project, while .exs files are scripts that are interpreted when run, commonly used for tests and configuration.