Gleam — Reference
Source: https://gleam.run/
Gleam
- Created: 2016 by Louis Pilfold; first release 2018; 1.0 released 2024-03-04
- Latest stable: Gleam v1.16.0 (2026-04-24)
- Status: Stable post-1.0 with strict semver and a no-breaking-changes-in-1.x commitment. Active monthly minor releases
- Paradigms: Pure functional, ML-family, immutable-by-default
- Typing: Static, strong, sound, Hindley-Milner with row-polymorphic records. No subtyping, no inheritance, no nulls, no exceptions in pure code
- Memory: Inherits from BEAM (preemptive concurrent garbage collector, per-process heaps, never stops the world). For JS target: V8/SpiderMonkey GC
- Compilation: Compiles to Erlang (then BEAM bytecode via
erlc) OR JavaScript (with TypeScript.d.tsdeclarations) - Primary domains: Backend services on BEAM, full-stack web (Lustre on JS), Erlang/Elixir interop layers, fault-tolerant distributed systems
- Official docs: https://gleam.run/ · Tour: https://tour.gleam.run/ · Cheatsheet: https://gleam.run/cheatsheets/
At a glance
Gleam puts a sound static type system on top of the BEAM (Erlang’s VM), giving you Erlang’s preemptive scheduler, hot code reload, distributed Erlang networking, and OTP supervision trees — but with a Rust-quality type checker that catches errors Erlang never could. Also compiles to JavaScript (with TS types), so you can ship one library to both Node/Deno/Bun and the browser. The language is intentionally tiny — no macros, no metaprogramming, no operator overloading. The trade-off is a small standard library you supplement with the Hex package ecosystem (which Gleam shares with Elixir/Erlang).
Getting started
Install: Single binary, no separate version manager, but asdf and mise plugins exist:
# macOS
brew install gleam erlang rebar3
# Linux: download from https://github.com/gleam-lang/gleam/releases
# Windows: scoop install gleam (also chocolatey, winget)
# Or via asdf:
asdf plugin add gleam && asdf install gleam latest && asdf global gleam latest
gleam --version # → gleam 1.16.0You also need Erlang/OTP for the BEAM target and Node (or Deno/Bun) for the JS target.
Hello world:
gleam new hello
cd hello
gleam run # → "Hello from hello!"The generated src/hello.gleam:
import gleam/io
pub fn main() -> Nil {
io.println("Hello from hello!")
}Project layout:
hello/
├── gleam.toml # manifest (deps, target)
├── manifest.toml # lockfile
├── src/hello.gleam # entry: pub fn main()
└── test/hello_test.gleam
Build/package tool: gleam is the entire toolchain.
gleam new NAME— scaffold projectgleam run— build + run maingleam test— run tests via gleeunit (default test runner)gleam build— compile onlygleam check— type-check only (fast)gleam format— canonical formattergleam docs build— generate HTML docsgleam add PKG— add a Hex package;gleam deps updategleam shell— Erlang/IEx-style shell against your projectgleam export erlang-shipment— produce a deployable BEAM bundlegleam publish— push to Hex
Target selection in gleam.toml:
target = "erlang" # or "javascript"Or per-command: gleam run --target javascript.
REPL: Indirectly — gleam shell drops you in Erlang’s shell with your project’s modules loaded. There’s no Gleam-syntax REPL.
Basics
Types & literals:
Int(arbitrary-precision on BEAM, doubles on JS within safe-integer range),Float,Bool(True/False),String(UTF-8),Nil(the unit type),BitArray(bit strings)List(a)(linked list),Result(a, e),Option(a)is NOT built-in — Gleam usesResulteverywhere- Tuples
#(a, b, c)are heterogeneous fixed-size - Records as custom types:
pub type User { User(name: String, age: Int) }— constructors are functions; field access via.name
Variables & scoping: Single-assignment, immutable, function-scoped. let declares; rebinding shadows:
let x = 1
let x = x + 1 // OK — shadowing creates a new bindinglet assert is the explicit “I know this matches” pattern that crashes if it doesn’t:
let assert Ok(value) = parse(input)Control flow: Everything is an expression.
-
casefor pattern matching (exhaustive, compile-time-checked):case x { 0 -> "zero" n if n > 0 -> "positive" _ -> "negative" } -
No
if/elsekeywords — usecaseon aBool -
No loops — recursion or stdlib higher-order functions
Functions:
pub fn add(a: Int, b: Int) -> Int {
a + b
}First-class. Labeled arguments (caller-side names): pub fn replace(in str: String, each pattern: String, with new: String) called as replace(in: "hi", each: "h", with: "b").
Strings: UTF-8, immutable. Concatenation: <> (e.g. "hello, " <> name). No printf-style interpolation — build with string.concat(["a", b]) or string.inspect(value). Multiline strings via consecutive \ns.
Collections: Linked List(a) is the canonical sequence. dict.Dict(k, v) from gleam/dict. set.Set(a) from gleam/set. No mutable arrays in pure Gleam — use BitArray for byte buffers, or call into Erlang/JS for a mutable structure.
Intermediate
Type system depth:
-
Custom types (algebraic data types):
pub type Shape { Circle(radius: Float) Rectangle(width: Float, height: Float) }Constructors create values; pattern-match in
case. Exhaustiveness enforced. -
Generics:
pub fn first(list: List(a)) -> Result(a, Nil)— type variables are lowercase, no annotations needed for definitions -
No subtyping, no overloading, no implicit conversions — language stays simple
-
Records are sugar for single-constructor custom types; field updates:
User(..user, name: "Bob")(record-update syntax)
Modules: Files are modules; src/foo/bar.gleam is module foo/bar. Public via pub. Imports:
import gleam/io
import gleam/list.{map, filter} // unqualified imports
import gleam/string as str // aliasError handling: No exceptions in pure Gleam. Convention: return Result(ok_value, error_value):
pub fn divide(a: Int, b: Int) -> Result(Int, String) {
case b {
0 -> Error("divide by zero")
_ -> Ok(a / b)
}
}The use syntax desugars callback-passing into linear-looking code. It’s Gleam’s monadic-do equivalent:
use file <- result.try(open("foo.txt"))
use contents <- result.try(read(file))
Ok(contents)This desugars to nested callbacks. The pattern works for any function whose last arg is a callback (result.try, list.try_each, task.await, etc.).
Concurrency: You get all of OTP, with types. The gleam_otp package wraps actors, supervisors, and gen_servers in typed APIs:
gleam/otp/actor— typed actor framework (replaces Erlang’s gen_server). Messages are typed; the compiler enforces every actor handles its message typegleam/otp/supervisor— supervision trees (one_for_one, rest_for_one, etc.)gleam/otp/task— fire-and-forget or await typed tasks- Process IDs (
Subject(message_type)) are typed by the message they accept — sending the wrong message to an actor is a compile error
This is the headline feature: BEAM’s actor model with end-to-end type safety.
I/O: gleam/io (println, debug), gleam/erlang/file (BEAM target only), simplifile (cross-target file I/O), gleam/http + mist/wisp for HTTP servers. JS target uses gleam/javascript/promise for Promise.
Stdlib highlights: gleam_stdlib is the official standard library and is intentionally minimal: list, dict, set, string, int, float, bool, result, option (yes, Option is here, just not built-in), iterator (lazy sequences), dynamic (untyped data + decoders), regex, uri, pair, function. Decoders for Dynamic are how you parse untyped Erlang data (e.g. JSON parses to Dynamic, you write a decoder to typed Gleam).
Advanced
Memory model — BEAM target: Each Erlang process has its own heap (typically a few KB), with a per-process generational GC. Per-process GC means no global stop-the-world — only a single process pauses, briefly. Inter-process communication is by message copy (or refcounted binaries for >64 bytes). On the JS target, you inherit V8’s GC.
Concurrency deep dive (BEAM): Erlang processes are user-space (not OS) processes with preemptive scheduling. Spawn millions cheaply. Distributed Erlang lets processes communicate across nodes transparently. Gleam wraps all of this — gleam_otp gives typed wrappers; raw Erlang process APIs are accessible via gleam/erlang/process. The headline feature: typed Subject(message_type) — sending the wrong message type doesn’t compile. Hot code upgrades work as in Erlang (with care).
FFI — external declarations: Call Erlang/Elixir/JavaScript from Gleam:
@external(erlang, "lists", "reverse")
@external(javascript, "./my_ffi.mjs", "reverse")
pub fn reverse(list: List(a)) -> List(a)The two @external annotations let one Gleam declaration target both runtimes. JS FFI files live in src/ next to the .gleam file. From Erlang/Elixir you can call Gleam modules directly — they look like ordinary Erlang modules with snake_case-named functions.
Reflection: Limited by design. gleam/dynamic lets you inspect untyped data with decoder combinators; string.inspect(value) produces a debug string for any value. There’s no runtime type info beyond what Erlang/JS expose for their own values.
Performance tools: On BEAM: :observer.start() (Erlang’s GUI tool), :recon for production introspection, :fprof/:eprof/:cprof profilers, :dbg tracer. Gleam compiles to small, idiomatic Erlang, so all BEAM tooling works as-is. On JS: standard Chrome DevTools / Node --inspect. gleam build --warnings-as-errors for CI.
God mode
Type system on top of BEAM — what you actually get:
- Typed actors:
actor.start(initial_state, handle_message)returns aSubject(msg_type). Sending the wrong msg type = compile error. Erlang has nothing like this; Elixir has dialyzer (which is structural and best-effort) - Typed supervisors:
supervisor.start_spec(spec)where children types are checked - Distributed-Erlang-aware: subjects can be sent across nodes; the type system tracks what messages each accepts
Two targets, one source:
target = "javascript"ingleam.toml→ output.mjsfiles inbuild/dev/javascript/<pkg>/- TypeScript declarations are generated alongside (
build/.../*.d.mts) - Same Gleam source can compile to both targets if it avoids target-specific FFI
- Conditional compilation via
@target(erlang)/@target(javascript)annotations on declarations
use syntax (sugar for monadic chaining): Read it as “use the next value, then continue”:
use a <- result.try(parse("42"))
use b <- result.try(parse("7"))
Ok(a + b)desugars to:
result.try(parse("42"), fn(a) {
result.try(parse("7"), fn(b) {
Ok(a + b)
})
})Works for any function whose final argument is a callback — no monad/applicative typeclasses needed.
No macros — and the rationale: Louis Pilfold has explicitly chosen no macros, no metaprogramming. Reasons: (1) compile-time safety — you can read any Gleam code top-to-bottom and know what it does, (2) tooling simplicity — LSP, formatter, and refactoring tools are dramatically easier, (3) ecosystem coherence — no custom DSLs fragmenting the community. Trade-off: things Elixir’s Ecto.Query or Rust’s serde_derive do via macros require explicit code in Gleam (decoders for JSON, query builders as values).
use + gleam_otp = typed-do-style actor code. The combination produces remarkably readable concurrent code:
use msg <- actor.receive(state, 1000)
case msg { ... }Hex package ecosystem (shared with Erlang/Elixir): ~16,000 packages on https://hex.pm/. Most Erlang/Elixir libs work in Gleam via thin wrapper packages (e.g. gleam_erlang, gleam_crypto). Searching for a Gleam-native package: https://packages.gleam.run/.
Custom build phases: Mostly handled by rebar3/mix under the hood when integrating with Erlang/Elixir projects. Pure-Gleam projects use only gleam. For Elixir interop in a mix project, use the mix_gleam Mix plugin.
Idioms & style
- Naming:
snake_casefor values/functions/modules,PascalCasefor types and constructors,SCREAMING_SNAKE_CASEis not idiomatic — usepub const my_constant = 42 - Formatter:
gleam formatis canonical, non-configurable — run on save. Required bygleam publish(won’t publish unformatted code) - Linter: Compiler warnings are aggressive —
--warnings-as-errorsfor CI. No separate linter; the type system + warnings cover most lint cases - Idiomatic patterns: prefer
ResultoverOptionfor fallible operations (carry an error reason), useusefor nestedResult.try/Task.await, build small focused modules, lean on the type system to make illegal states unrepresentable, write decoders forDynamicdata instead of trusting external schemas - OTP idioms: prefer
gleam_otptyped wrappers over rawgleam/erlang/process; use aSubject(MyMessages)per actor; use supervisors over manual restart logic - Expert review focus: correct error type carried in
Results (don’t make everythingResult(a, String)), nolet assertoutside tests, exhaustivecase(compiler enforces but watch for warnings), proper supervision strategy, target-specific FFI behind a stable Gleam-typed shim
Ecosystem
| Domain | Library |
|---|---|
| Web framework (server) | Wisp (over Mist), Mist (HTTP server), glen |
| Web framework (full-stack) | Lustre (Elm-architecture frontend, compiles to JS — also runs server-side) |
| HTTP client | gleam_http, gleam_httpc, lustre_http |
| Database | gleam_pgo (Postgres), gleam_redis, squirrel (typed SQL via Postgres preparation) |
| JSON | gleam_json (parse to/from Dynamic) |
| Templating | lustre/element/html (typed HTML), glentities |
| Testing | gleeunit (default; ExUnit-style), birdie (snapshot testing) |
| OTP / actors | gleam_otp (official typed wrappers) |
| Distributed systems | Use raw :gleam/erlang/process + Distributed Erlang facilities |
| Docs | gleam docs build → published to https://hexdocs.pm/ |
Notable users: Strand (Rust + Gleam observability), Octavius (Lustre full-stack), various BEAM consultancies. Gleam community on Discord is active and well-moderated.
Gotchas
- No
Option/Some/Nonebuilt in —Optionis ingleam/option. Most APIs returnResult(a, e)instead - No
if— onlycase. Some find this annoying initially; it’s a deliberate small-language choice - Ints differ between targets: BEAM ints are arbitrary-precision; JS ints are doubles in safe-integer range (
±2^53). Code that overflows one target may not overflow the other usedesugars to nested closures — stack traces and reading mid-pipeline values can be confusing until you internalize the desugaring- No macros means more boilerplate for serialization, query building, etc. — write the code or use a code-gen tool like
squirrel - Gleam ↔ Erlang interop is one-way-typed: Erlang code calling Gleam is unchecked (it’s just an Erlang module to Erlang); Gleam calling Erlang requires
externaldeclarations with manual type signatures (the compiler trusts you) - Hot code upgrades work but require designing actors with
code_change/3callbacks (typed wrappers for this are still maturing) - JS target’s stdlib has gaps vs the BEAM target — some
gleam_erlang/*modules have no JS equivalent let assertwill crash at runtime if the pattern doesn’t match — use only for invariants you’ve proven, prefercase+Errorfor fallible parsing- Exhaustiveness is at compile time — but adding a constructor to a
pub typein a library is a breaking change for downstreamcaseexpressions. Plan API evolution carefully - Performance: Gleam compiles to idiomatic Erlang (or readable JS); it’s as fast as Erlang for BEAM workloads. NOT a numerical computing language — for that, drop down to Erlang’s NIFs (Rustler, Zigler) and call from Gleam
gleam shellputs you in Erlang’s shell, not a Gleam REPL — there is no Gleam-syntax REPL
Citations
- Gleam official site: https://gleam.run/
- Tour: https://tour.gleam.run/
- Cheatsheets (for Elixir/Elm/Erlang/PHP/Python/Rust devs): https://gleam.run/cheatsheets/
- News & releases: https://gleam.run/news/
- v1.16.0 release (2026-04-24): https://github.com/gleam-lang/gleam/releases/tag/v1.16.0
- Standard library docs: https://hexdocs.pm/gleam_stdlib/
- gleam_otp docs: https://hexdocs.pm/gleam_otp/
- Gleam packages: https://packages.gleam.run/
- Hex registry: https://hex.pm/
- Lustre (frontend framework): https://hexdocs.pm/lustre/
- Wisp (web framework): https://hexdocs.pm/wisp/