The BEAM VM has been running production systems with near-perfect uptime for decades. Erlang gave us WhatsApp handling 900 million users on a team of 50 engineers. Elixir made BEAM accessible to a generation of Ruby developers. Both are dynamically typed — an entire class of bugs only surfaces at runtime, in production, when users are already affected.
Gleam solves that. It is a statically typed functional language that compiles to BEAM bytecode (and JavaScript), bringing lightweight processes, fault-tolerant supervision trees, and actor-based concurrency under a type system that catches errors at compile time.
After shipping distributed services on Erlang, Go, and Elixir stacks, one pattern keeps showing up: runtime type errors are the bug that pages you at 3am. They hide in rarely-exercised code paths, survive testing, and only appear under real production load. Gleam attacks that problem directly — static typing baked into the language, not bolted on as an afterthought.
Approach Comparison — Gleam, Elixir, and Erlang Side by Side
Most distributed system failures trace back to two things: wrong assumptions embedded in code, and processes that don’t recover when they crash. Your language choice shapes how well you can defend against both.
Erlang — The Gold Standard for Reliability, Hard to Love
Erlang is the original BEAM language. It runs telephony infrastructure, message queues, and databases like RabbitMQ and CouchDB. Its process model is unmatched. But the syntax is alien to most developers, the type system is optional and incomplete (Dialyzer helps, but never enforces), and the learning curve is brutal.
Elixir — Beautiful Syntax, Runtime Type Errors
Elixir fixed Erlang’s ergonomics problem with Ruby-like syntax, an excellent ecosystem (Phoenix, Ecto, LiveView), and a thriving community. But it is dynamically typed. You can write String.upcase(42) and only find out it is wrong when that code path executes in production — possibly at 2am on a Friday.
Gleam — Static Typing on BEAM
Gleam takes a third path. ML-inspired syntax, a Hindley-Milner type system (similar to Haskell and Rust’s type inference), and full interoperability with Erlang and Elixir code. Compile-time guarantees plus BEAM’s runtime characteristics. Not a trade-off — both at once.
Compare how error handling looks across the three languages:
% Erlang — no compiler enforcement on the call site
divide(A, B) ->
case B of
0 -> {error, division_by_zero};
_ -> {ok, A div B}
end.
# Elixir — similar shape, still dynamically typed
def divide(a, b) do
case b do
0 -> {:error, :division_by_zero}
_ -> {:ok, div(a, b)}
end
end
// Gleam — compiler forces you to handle both Ok and Error
pub fn divide(a: Int, b: Int) -> Result(Int, String) {
case b {
0 -> Error("Division by zero")
_ -> Ok(a / b)
}
}
Call divide() in Gleam and ignore the Error case. The code does not compile. Not a warning — a hard stop before a single byte ships to production.
Pros and Cons of Gleam
What Gleam Gets Right
- Compile-time exhaustiveness checking: Pattern matches must cover all cases. Miss a branch and the compiler tells you before deployment.
- Full Erlang ecosystem interop: Call any Erlang or Elixir library from Gleam via FFI. RabbitMQ clients, database drivers, Phoenix — all accessible. You are not starting from zero.
- JavaScript compilation: Gleam compiles to JavaScript too, so you can share business logic between backend and frontend with one codebase.
- Readable compiler errors: Error messages are written to be understood by humans, not just parsed by IDEs. They tell you what went wrong and often suggest how to fix it.
- Labeled arguments enforced: Multi-parameter function calls require labels at the call site, making code self-documenting without extra comments.
Where Gleam Still Has Rough Edges
- Small but growing ecosystem: Compared to Elixir’s mature package index, Gleam’s library selection is limited. Expect to FFI into Erlang for some things.
- OTP abstractions are newer: The
gleam_otplibrary wraps GenServer and Supervisor, but the ergonomics are not yet as polished as Elixir’s. - No macros: Gleam deliberately avoids metaprogramming. If your codebase leans heavily on Elixir macros, you will need to restructure that thinking.
- Tooling still maturing: VS Code and Zed have extensions, but the ecosystem is not yet as refined as Go or Rust tooling.
Recommended Setup
Erlang needs to go in first (OTP 25+ recommended), then Gleam itself.
# macOS with Homebrew
brew install erlang gleam
# Ubuntu/Debian — install Erlang first, then Gleam binary
apt install erlang
# Download the Gleam binary from the GitHub releases page
# or use asdf for version management:
asdf plugin add gleam
asdf install gleam latest
asdf global gleam latest
# Verify both are working
gleam --version
erl --version
Create a new project and add OTP dependencies:
gleam new fault_tolerant_worker
cd fault_tolerant_worker
Edit gleam.toml to add the OTP packages:
[dependencies]
gleam_stdlib = ">= 0.34.0 and < 2.0.0"
gleam_otp = ">= 0.10.0 and < 1.0.0"
gleam_erlang = ">= 0.25.0 and < 1.0.0"
gleam deps download
Implementation Guide — A Fault-Tolerant Actor from Scratch
The fundamental unit of fault tolerance on BEAM is the actor — a lightweight process with isolated state and a message inbox. Here is what building one looks like in Gleam.
Defining the Actor
Each actor needs a message type (an algebraic data type), a state, and a handler function. The type system ensures every message variant is handled:
// src/counter.gleam
import gleam/erlang/process
import gleam/otp/actor
pub type Message {
Increment(amount: Int)
Reset
GetCount(reply_to: process.Subject(Int))
}
pub fn start() -> Result(process.Subject(Message), actor.StartError) {
actor.start(0, handle_message)
}
fn handle_message(
message: Message,
count: Int,
) -> actor.Next(Message, Int) {
case message {
Increment(amount) -> actor.continue(count + amount)
Reset -> actor.continue(0)
GetCount(client) -> {
process.send(client, count)
actor.continue(count)
}
}
}
Add a new variant to Message and forget to handle it in handle_message. The compiler refuses to build. That is exhaustiveness checking — catching a future production incident before the code ever runs.
Using the Actor
// src/fault_tolerant_worker.gleam
import gleam/erlang/process
import gleam/io
import gleam/int
import counter
pub fn main() {
let assert Ok(counter_pid) = counter.start()
// Fire-and-forget messages
process.send(counter_pid, counter.Increment(amount: 5))
process.send(counter_pid, counter.Increment(amount: 3))
// Synchronous call — send a reply subject, wait for response
let reply_subject = process.new_subject()
process.send(counter_pid, counter.GetCount(reply_to: reply_subject))
let count = process.receive(reply_subject, 1000)
case count {
Ok(n) -> io.println("Count: " <> int.to_string(n))
Error(_) -> io.println("Timeout waiting for count")
}
}
gleam run
# Count: 8
Adding a Supervisor for Automatic Recovery
Supervision is where BEAM systems earn their reputation. If the counter actor crashes — from a bug, an unexpected message, or a resource failure — the supervisor automatically restarts it with fresh state:
// src/app_supervisor.gleam
import gleam/otp/supervisor
import counter
pub fn start() {
supervisor.start(fn(children) {
children
|> supervisor.add(
supervisor.worker(fn(_) { counter.start() })
)
})
}
This is the “let it crash” philosophy that makes BEAM systems so resilient. Skip the defensive error handling scattered across every function. Build self-healing process hierarchies instead. An actor hits an unexpected state, crashes cleanly, the supervisor notices, and a fresh instance is running — often before anyone gets paged.
Calling Erlang Libraries Directly
When Gleam does not have a native wrapper for an Erlang library, FFI is one line:
// Bind directly to Erlang's :timer module
@external(erlang, "timer", "sleep")
pub fn sleep(milliseconds: Int) -> Nil
// Now usable like any Gleam function
pub fn delayed_reset(counter_pid) {
sleep(5000)
process.send(counter_pid, counter.Reset)
}
Is Gleam the Right Choice for Your Next Project?
Gleam 1.0 shipped in March 2024. The core language spec is stable and locked. For greenfield distributed systems where you want high concurrency, fault isolation, and hot code reloading — with static type safety on top — Gleam is a real production option today.
If your team already uses Elixir and dynamic typing has not caused you pain, there is no urgent reason to switch. But if you are starting a new distributed system from scratch, or runtime type errors have burned you before, Gleam sits in a position no other language occupies. Static typing. Process isolation. Supervision trees. All in one package.
Rust gives you type safety but not BEAM’s lightweight process model. Go gives you solid concurrency but not fault-tolerant supervision trees. Haskell gives you a strong type system but not decades of distributed systems infrastructure. Gleam gives you all three — and once you have run a system that heals itself automatically after a crash, the alternatives start feeling like compromises.
