Gleam: Type Safety on the BEAM
The Erlang VM has a deserved reputation for indestructibility. WhatsApp ran on it at two billion users. Ericsson built the AXD301 switch — nine-nines uptime — on it in the 1990s. The BEAM’s preemptive scheduler, per-process garbage collection, and actor-model concurrency represent decades of hard-won production wisdom that no new runtime is going to replicate overnight.
The price of admission has always been a dynamic type system. Erlang and Elixir are expressive, powerful languages, but the feedback loop is runtime-first: dialyzer helps, but it’s opt-in, post-hoc analysis, not a compiler that refuses to produce a binary until your types make sense. Teams that want the BEAM’s legendary fault tolerance but also want a type system that catches the class of errors that crash production services at 3 AM have historically had no good answer.
Gleam is that answer — or the most credible attempt at one yet.
Released at version 1.0 in March 2024 after years of development by Louis Pilfold, Gleam is a statically typed functional language that compiles to Erlang bytecode (running on the BEAM) and to JavaScript (running in browsers and Node). It has Hindley-Milner type inference, sum types, exhaustive pattern matching enforced by the compiler, no null, no exceptions, and full interoperability with the existing Erlang and Elixir ecosystem. As of April 2026 it is at v1.15, actively maintained, and picking up real production users.
This post goes deep. We will cover the type system, the syntax, the use expression (Gleam’s most distinctive feature), error handling, the BEAM runtime, Erlang/Elixir interop, the JavaScript target, the tooling, building HTTP services with Wisp, and an honest look at where Gleam stands today versus the alternatives.
What Gleam Is
Gleam is a compiled, statically typed, functional language. The type system is a variant of Hindley-Milner (the same family as OCaml, Haskell, and Elm), with full type inference — you rarely need to write type annotations, the compiler figures them out. Where you do write them (on public function signatures, for documentation), the compiler verifies them.
The core pitch is not subtle: take everything the BEAM is already excellent at — concurrency, fault tolerance, distribution, hot code loading, battle-tested OTP patterns — and add a compiler that refuses to let you ship code with type errors, null pointer dereferences, unhandled error paths, or incomplete pattern matches.
|
|
That is a complete Gleam program. It compiles to Erlang, runs on the BEAM, and you get the entire OTP ecosystem for free.
The Creator and History
Louis Pilfold started Gleam around 2018. The language spent several years in pre-1.0 development, which was genuinely productive — the pre-release period was used to get the type system right, iterate on syntax, and build the toolchain. The 1.0 milestone in March 2024 was not a marketing exercise; it represented a commitment to semantic versioning and backwards compatibility. The language design, compiler, build tool, package manager, formatter, and language server all reached stability simultaneously.
Since 1.0, development has been consistent and disciplined: roughly monthly releases, each focused on either developer experience improvements (better error messages, language server features, code actions) or performance (v1.11 brought a 30% JavaScript performance improvement). The language has resisted feature bloat. By design, Gleam is a small language — the tour claims you can learn the whole thing in an afternoon. That is mostly true, and it is a deliberate choice.
The Core Value Proposition
If you are coming from Elixir, Gleam gives you:
- A compiler that will not let you forget to handle the
Errorbranch of aResult - No runtime crashes from “function clause” errors on unexpected input
- Types that document your data structures and are verified, not just aspirational
- Exhaustive pattern matching — add a variant to a type and the compiler tells you every
caseexpression that needs updating
If you are coming from Haskell or OCaml, Gleam gives you:
- The BEAM runtime — lightweight processes, preemptive scheduling, per-process GC
- OTP supervision trees, actors, hot code reloading, distributed Erlang
- A practical language that ships, not a research vehicle
- Access to the Hex package ecosystem (thousands of Erlang and Elixir packages)
If you are coming from Go or Rust, Gleam gives you:
- Functional-first programming with immutable data
- A type system stronger than Go’s and friendlier than Rust’s
- Concurrency through the actor model rather than goroutines or async/await
- The same “if it compiles, it mostly works” ergonomics Rust programmers value
The Type System
Gleam’s type system is Hindley-Milner with some practical extensions. The key properties:
- Full type inference: The compiler infers types for everything. You can write a whole module without a single annotation and the compiler will still catch type errors.
- No implicit coercions: Int and Float are different types. There is no automatic widening or narrowing.
- No null: The
Optiontype represents absence. The compiler forces you to handle it. - No exceptions: All errors are
Resultvalues. The compiler forces you to handle them. - Exhaustive pattern matching: Every
caseexpression must cover all possible inputs. The compiler rejects incomplete matches.
Primitive Types
|
|
The float operator distinction (+., -., *., /.) looks unusual to newcomers but is deliberate: it eliminates an entire class of bugs where integer and float arithmetic get accidentally mixed. The compiler enforces it.
Custom Types (Sum Types / ADTs)
Custom types are Gleam’s primary way to model domain data. They are algebraic data types (ADTs) in the ML tradition.
|
|
If you add a Pentagon variant to Shape without updating area, the compiler will tell you exactly which case expressions need updating. This is one of the most practically valuable properties of the type system.
Record Types
Custom type variants can hold named fields, functioning like records or structs:
|
|
Records in Gleam are not mutable. User(..user, role: Admin) creates a new User value with all fields copied from user except role, which gets the new value. This is the standard functional programming pattern.
The Option Type
There is no null in Gleam. Absent values are represented with Option:
|
|
Option is a generic custom type. Some(42) is an Option(Int). None is an Option(a) for any a. The compiler will not let you use an Option(String) where a String is expected — you must unwrap it explicitly:
|
|
The Result Type
Fallible operations return Result(ok_type, error_type):
|
|
|
|
Unhandled Result values cause a compiler warning. The compiler tracks whether you have checked both Ok and Error branches in your pattern matches. There is no way to silently swallow an error — you must either handle it explicitly, propagate it, or call result.unwrap with a default.
Generic Types
Gleam supports parametric polymorphism with type variables (lowercase):
|
|
The compiler infers type variable substitutions at call sites. If you call push(my_int_stack, "hello"), the compiler rejects it — the element type is already fixed to Int for that particular stack.
Type Aliases
Type aliases create alternate names for existing types. The aliased type and the original type are fully compatible:
|
|
Opaque Types
Opaque types hide their constructors from other modules, enabling the smart constructor pattern for maintaining invariants:
|
|
External code can only create PositiveInt through the new constructor, which validates the invariant. The compiler prevents bypassing it.
Syntax and Core Language
Gleam’s syntax is ML-inspired, leaning toward the Rust/OCaml end of the family rather than Haskell’s. Braces instead of significant whitespace, fn for functions, let for bindings. If you have written OCaml, F#, or Elm, it will feel immediately familiar.
Let Bindings
|
|
The “shadowing is allowed but mutation is not” model takes a few days to internalize if you come from imperative languages. Once it clicks, the constraint is freeing — you can always reason about a binding’s value by looking at where it was created.
Functions
|
|
Functions are first-class values. They can be passed to and returned from other functions. Function types are written fn(arg_types) -> return_type.
Labeled Arguments
Gleam supports labeled arguments, which improve call-site readability for functions with multiple parameters:
|
|
When the variable name at the call site matches the label, you can use shorthand:
|
|
The Pipeline Operator
|> passes the result of the left expression as the first argument to the right function. It is the idiomatic way to chain transformations:
|
|
The pipeline operator is syntactic sugar — a |> f(b) desugars to f(a, b) where a is inserted as the first argument. The Gleam compiler will try the pipeline as first argument first, and if that doesn’t type-check, it tries it as a standalone call (for functions that take one argument).
Pattern Matching with case
case is Gleam’s primary control flow mechanism. It is not an afterthought — it is the main way you make decisions in Gleam code.
|
|
The compiler guarantees exhaustiveness. If you remove a branch, add a new custom type variant, or write a guard that leaves gaps, the compiler tells you at build time.
String Interpolation
Gleam added string interpolation in a relatively recent release:
|
|
For older patterns (pre-interpolation), the <> concatenation operator and string.concat are used:
|
|
Blocks
Expressions can be grouped into blocks. The last expression in a block is the block’s value:
|
|
Blocks are how you sequence multiple operations where only a single expression is syntactically expected.
Recursion and Tail Calls
Gleam has no loops — iteration is done through recursion. The compiler optimizes tail-recursive functions into loops, so you can recurse over large data structures without stack overflow:
|
|
In practice, most list operations are handled by gleam/list — map, filter, fold, find, sort — all of which are already tail-recursive. You write your own loops mainly when the standard library doesn’t cover your specific traversal pattern.
The use Expression
The use expression is Gleam’s most distinctive feature, and probably the most important one to understand if you are coming from another language. It is worth spending time on.
The Problem: Callback Hell in a Typed Language
Gleam has no exceptions, no early returns, and no do-notation. Pure functional control flow means nested callbacks. Consider a realistic request handler:
|
|
This is syntactically correct Gleam. It is also unreadable. Every early-exit path requires another level of nesting.
The Solution: use Desugars Callbacks
use is syntactic sugar. The expression:
|
|
desugars to:
|
|
The key insight: everything that comes after the use expression in the same block becomes the body of an anonymous function passed as the last argument to some_function. This means use works with any function that takes a callback as its last argument — no magic, no special protocol.
Here is the same handler rewritten with use:
|
|
Same semantics, radically different readability. The first version hides the happy path deep in nested callbacks. The second version reads like a linear sequence of steps with early-exit behavior handled by the callback structure.
use with Result Chaining
The most common use of use in practice is chaining Result-returning operations:
|
|
result.try has the signature fn(Result(a, e), fn(a) -> Result(b, e)) -> Result(b, e). If the result is Ok(value), it calls the callback with value. If it is Error(e), it short-circuits and returns Error(e). Combined with use, this gives you Haskell’s do-notation for Result without needing monads as a language concept.
Each step either succeeds and passes its value to the next step, or fails and returns the error immediately. The error type must be consistent across the chain (or you need result.map_error to align them).
use for Resource Management
use is also perfect for resource management — the callback pattern ensures cleanup runs regardless of what happens in the body:
|
|
The transaction commits or rolls back automatically based on whether the block returns Ok or Error. No try/finally, no defer, no explicit cleanup — the callback pattern handles it.
use with Other Patterns
use works with any function that takes a callback as its last argument. This makes it universally applicable:
|
|
How use Compares to Other Languages
| Language | Feature | Notes |
|---|---|---|
| Haskell | do-notation |
Requires monad typeclass, type class system |
| F# / OCaml | Computation expressions / let* |
Similar desugaring, requires special syntax |
| Rust | ? operator |
Only for Result/Option, not general |
| JavaScript | async/await |
Only for Promises |
| Gleam | use |
Works with any callback-taking function, no typeclasses needed |
Gleam’s use is more general than ? (works for any callback pattern) but more explicit than do-notation (the callback structure is visible in the desugaring). It is a pragmatic middle ground that works well in practice.
Error Handling
Gleam’s error handling philosophy: errors are values. There are no exceptions — not even for “exceptional” circumstances. If an operation can fail, it returns Result(success, failure). The type system forces you to decide what to do with failures.
The Result Type in Practice
|
|
The gleam/result Module
The standard library’s result module provides a comprehensive set of combinators:
|
|
Error Propagation with use
The pattern for propagating errors up a call stack without writing Error(err) -> Error(err) repeatedly:
|
|
This is clean. The happy path reads top-to-bottom. Errors short-circuit immediately. No nesting.
When Errors Must Be Final: let assert and panic
Sometimes you genuinely know at a certain point that a value must be in a particular shape — you have already validated it elsewhere, or you are in a test, or you are writing a bootstrap function where failure is unrecoverable. Gleam provides escape hatches:
|
|
let assert and panic are deliberate escape hatches, not routine error handling. The convention is to use them only where you can document why the assertion is guaranteed to hold, or where a crash is genuinely the correct behavior (unrecoverable initialization failure, test assertion, logic invariant violation).
Why No Exceptions?
The practical argument: exceptions create implicit control flow paths that are invisible in function signatures. A function that raises an exception is not distinguishable from one that does not by looking at its type. This means callers cannot know which operations can fail, which creates hidden failure modes that show up in production.
Result makes failure explicit in the type. If a function returns Result(User, DbError), you know it can fail, you know what it can fail with, and the compiler makes you handle both cases. The codebase becomes self-documenting about its failure modes.
The BEAM Runtime
Understanding what Gleam gets “for free” from targeting the BEAM requires understanding what the BEAM actually is.
The BEAM Is Not an Ordinary VM
The BEAM (Bogdan/Björn’s Erlang Abstract Machine) was designed from the ground up for telecommunications systems — specifically, Ericsson’s telephone exchanges, which required:
- No downtime: Systems had to keep running even during software upgrades
- Massive concurrency: Millions of simultaneous phone calls
- Fault isolation: One bad call should not crash the exchange
- Predictable latency: No multi-second GC pauses
Every design decision in the BEAM flows from these requirements, and they happen to map perfectly onto modern distributed web services.
Preemptive Scheduling
The BEAM uses a preemptive scheduler, not a cooperative one. Every Erlang/Gleam process gets a limited number of “reductions” (roughly, function calls) before the scheduler preempts it and runs something else. Processes cannot monopolize CPU time.
This is fundamentally different from Go’s goroutines (cooperative until 1.14, and still partially cooperative), Node.js’s event loop (cooperative, a blocking operation blocks everything), or Python’s asyncio (cooperative). On the BEAM, if one process goes into an infinite loop, all other processes continue running normally. The misbehaving process just gets CPU-starved or killed.
Traditional Thread Model:
Thread 1: ████████████████████────────────────
Thread 2: ────────────────────████████████████
(one blocks, the other waits)
BEAM Scheduler (N schedulers for N CPU cores):
Scheduler 1: P1 P2 P3 P4 P1 P2 P3 P4 P5 P1...
Scheduler 2: P6 P7 P8 P9 P6 P7 P8 P9 P6 P7...
(each scheduler round-robins hundreds of processes)
Lightweight Processes (Actors)
BEAM processes are not OS threads. A fresh BEAM process starts with a ~2KB heap. A modern system can run millions of them simultaneously. The overhead per process is so low that the idiomatic pattern is to create a process per connection, per user session, per request — whatever the natural unit of work is.
This is the actor model: each process has private state, communicates only through message passing, and can only affect other processes by sending messages. There is no shared mutable state between processes (though within a process you can use mutable state through the process’s own state mechanism).
|
|
Per-Process Garbage Collection
Each BEAM process has its own heap and garbage collector. When a process is done (or killed), its entire heap is reclaimed in one step — no global GC pause. When a process is active but idle (waiting for a message), it is not scanned by the GC at all.
This gives the BEAM a unique property: under load, GC pauses do not increase globally. More active processes means more total GC work, but the pauses per process stay small and bounded. The “GC pause at the worst possible moment” problem that plagues JVM applications is structurally absent.
OTP: The Framework for Reliable Systems
OTP (Open Telecom Platform) is the set of libraries and design patterns built on top of the BEAM’s process model. It provides:
- GenServer / Actor: A generic server pattern that handles OTP system messages, hot code upgrades, and debugging hooks
- Supervisor: Processes that watch other processes and restart them when they crash
- Application: The top-level abstraction for a running OTP application with its own supervision tree
- Registry: Named process lookup without passing PIDs around
The key OTP insight is let it crash: instead of defensive programming (catching every possible error, validating all inputs, handling every edge case), you write the happy path and let supervisors handle failures by restarting the failed process in a known-good state.
Supervision Tree Example:
Application
├── DatabaseSupervisor
│ ├── ConnectionPool (restarts on crash, replaces broken connections)
│ └── MigrationRunner (runs once, exits normally)
├── WebSupervisor
│ ├── HttpListener (restarts on crash)
│ └── RequestHandlerPool
│ ├── Handler_1 (restarts on crash, independent)
│ ├── Handler_2 (restarts on crash, independent)
│ └── Handler_N...
└── BackgroundSupervisor
├── EmailWorker (restarts on crash)
└── MetricsReporter (restarts on crash)
A crash in Handler_2 kills that process and the supervisor restarts it. Handler_1 keeps running. The database connection is unaffected. The system degrades gracefully under partial failure rather than crashing entirely.
What Gleam Gets From the BEAM
All of this — preemptive scheduling, lightweight processes, per-process GC, OTP supervision — is available to Gleam programs. A Gleam program targeting Erlang compiles to BEAM bytecode and runs on the BEAM VM. It is interoperable with Erlang and Elixir code at the process level. It participates in supervision trees. It can be distributed across nodes.
The type safety Gleam adds does not come at the cost of runtime characteristics. A Gleam program is not slower than an equivalent Erlang program — the compiled output is just Erlang (or BEAM bytecode).
The gleam_otp Library
Gleam provides typed OTP abstractions through the gleam_otp package. The API is type-safe but semantically equivalent to Erlang’s OTP.
Building Typed Actors
|
|
The type system ensures you can only send CounterMessage values to this actor. Send the wrong type? Compiler error at the call site.
Supervision Trees
|
|
When any of those workers crashes, the supervisor automatically restarts it. The restart strategy (one-for-one, one-for-all, rest-for-one) determines how failures propagate.
Erlang and Elixir Interoperability
One of Gleam’s most significant practical advantages is its interoperability with the existing Erlang ecosystem. You are not starting from scratch — you have access to two decades of battle-tested libraries.
The @external Attribute
Foreign function interface (FFI) in Gleam is done with @external. It maps a Gleam function signature to an Erlang function:
|
|
The first string argument to @external(erlang, ...) is the Erlang module name, the second is the function name. The arity is inferred from the Gleam function signature.
|
|
The gleam/erlang Standard Package
The gleam_erlang package provides typed bindings to common Erlang standard library functionality:
|
|
Calling Elixir from Gleam
Elixir code compiles to BEAM bytecode and is callable from Gleam using @external. The naming convention: Elixir modules are prefixed with Elixir. in the BEAM namespace:
|
|
In practice, calling Elixir from Gleam is more common in hybrid projects where you are gradually introducing Gleam into an existing Elixir codebase. For new projects, you would use Gleam-native libraries where they exist.
Using Hex Packages from Erlang/Elixir
Since Gleam uses Hex (the Erlang/Elixir package registry), you can add Erlang packages directly to your gleam.toml:
|
|
Then wrap the Erlang functions with Gleam types:
|
|
For HTTP clients specifically, the Gleam ecosystem has its own typed alternatives (gleam_http, gleam_fetch) that are more idiomatic.
Multi-Target Externals
When writing code that must run on both BEAM and JavaScript, you can provide target-specific implementations:
|
|
The JavaScript Target
Gleam compiles to both Erlang bytecode and JavaScript. This is a first-class target, not an afterthought — the standard library works on both targets, and the type system applies equally.
Why JavaScript?
The JavaScript target unlocks two use cases:
- Full-stack Gleam: Share types and business logic between backend (BEAM) and frontend (browser). Define your domain types once, use them on both sides.
- JavaScript-first environments: Some teams want Gleam’s type system but are committed to Node.js or Deno for infrastructure reasons. The JS target supports this.
Setting the Target
|
|
You can also specify target per-build:
|
|
JavaScript FFI
When targeting JavaScript, @external maps to JavaScript modules:
|
|
The FFI module is a standard ES module:
|
|
TypeScript Definitions
Gleam generates TypeScript .d.ts files alongside the compiled JavaScript. This means TypeScript code can safely consume Gleam modules with full type information:
|
|
|
|
Lustre: Frontend Framework
The primary frontend framework for Gleam is Lustre, which implements The Elm Architecture (TEA) — the same architecture that Redux was inspired by:
|
|
Lustre also supports server-side rendering and real-time server components, making it viable for both SPA and SSR use cases.
Shared Code Between BEAM and Browser
The ability to share types across targets is one of Gleam’s most compelling practical features:
|
|
The backend validates and constructs these types. The frontend decodes API responses into the same types. If the API contract changes, both break at compile time.
Ecosystem and Tooling
Gleam has a well-designed, batteries-included toolchain. A single binary handles everything.
Project Creation
|
|
|
|
Core Commands
|
|
manifest.toml
manifest.toml is the lockfile — pinned exact versions of all dependencies, transitive included. Commit it. Reproducible builds across developer machines and CI.
|
|
Testing with Gleeunit
The default test framework is gleeunit, which wraps EUnit:
|
|
Key Ecosystem Packages
Web Servers and Frameworks:
| Package | Description |
|---|---|
wisp |
Practical HTTP framework for backend services |
mist |
Low-level HTTP server (Wisp’s underlying server) |
lustre |
Frontend framework implementing TEA; also does SSR |
dream |
Alternative web framework |
Data and Encoding:
| Package | Description |
|---|---|
gleam_json |
JSON encoding/decoding |
decode |
Composable decoders for dynamic data |
gleam_http |
HTTP types (shared between client and server) |
gleam_fetch |
HTTP client for JavaScript target |
Database:
| Package | Description |
|---|---|
squirrel |
Type-safe SQL queries (generates Gleam code from SQL) |
postgleam |
Native PostgreSQL driver |
Testing:
| Package | Description |
|---|---|
gleeunit |
Default test runner wrapping EUnit |
birdie |
Snapshot testing |
OTP and Concurrency:
| Package | Description |
|---|---|
gleam_otp |
Typed actors, supervisors, tasks |
gleam_erlang |
BEAM process, atom, port abstractions |
Building HTTP Services with Wisp
Wisp is the go-to HTTP framework for Gleam backend services. It is practical, not magical — built on Mist (a pure-Gleam HTTP server built on Mist, which wraps ranch, the Erlang TCP server). Let’s build a complete API service.
Project Setup
|
|
|
|
Domain Types
|
|
JSON Encoding and Decoding
|
|
Routing and Request Handling
|
|
Application Entry Point
|
|
Testing Handlers
Wisp includes wisp/simulate for testing handlers without starting a real server:
|
|
An Honest Assessment
Gleam 1.0 is a real milestone. The language is genuinely good. But honest assessment requires looking at where it stands today versus the alternatives, not just where it is headed.
Where Gleam Genuinely Shines
Greenfield BEAM services where type safety matters most. If you are starting a new service and your team values type safety, Gleam is a compelling choice. You get the BEAM’s runtime characteristics — the scalability, the fault tolerance, the actor model — and you get a compiler that will not let you ship a service that crashes because someone passed a None where a value was expected. This combination does not exist anywhere else.
Small-to-medium teams building long-lived systems. The type system pays dividends over time. A codebase you have to maintain for two years benefits enormously from exhaustive pattern matching — add a new variant to your order status type and the compiler tells you every handler that needs updating. In Elixir, that is a grep and prayer exercise.
Full-stack teams who want to share types between backend and frontend. The JavaScript compilation target is genuinely useful here. Define your API contract types in Gleam, compile them to BEAM for your backend and JavaScript for your frontend. Type mismatches become compile errors rather than runtime failures that only manifest when a user hits a particular edge case.
Teams coming from typed functional languages (Elm, Haskell, OCaml) who want production systems. Gleam is a practical language for building real services. If you love Elm’s type system but want to write a backend, Gleam on the BEAM is the closest thing to Elm on the server.
Gradual adoption in existing Elixir codebases. Because Gleam and Elixir are both BEAM languages, you can introduce Gleam modules into an existing Elixir application. The interop is not seamless (dynamic typing at the boundary is unavoidable) but it is workable. Teams have successfully written new modules in Gleam while the rest of the application stays in Elixir.
Where Elixir/Erlang Is Still Better
Ecosystem maturity. Elixir’s ecosystem is dramatically larger. Phoenix alone has years of production hardening, LiveView, Channels, a mature router, PubSub, and a vast body of documentation, blog posts, and community knowledge. Ecto is a battle-tested data layer. The Hex ecosystem for Elixir is an order of magnitude richer than what Gleam has today.
Phoenix LiveView specifically. LiveView is genuinely remarkable — real-time server-rendered UIs without JavaScript, with years of production deployments behind it. There is no Gleam equivalent that comes close. If LiveView is what you need, use Elixir.
Operator familiarity. Most engineers in the BEAM ecosystem know Elixir. Hiring is easier, stackoverflow answers exist, tutorials exist. Gleam’s community is small, though friendly and growing.
Pattern matching on function heads. Elixir lets you define multiple function clauses matching different patterns:
|
|
Gleam requires a single function with a case body. This is not strictly worse — case expressions are more explicit — but Elixir’s approach can be more concise for certain patterns.
Dialyzer/Gradient for gradual typing. If you want type safety in an existing Elixir codebase, tools like Dialyzer (with type specs), Gradient, and the experimental Elixir type system (the Erlang typechecker integration announced for Elixir 1.17+) can get you partway there without a full rewrite.
The 1.0 Milestone and What It Means
The 1.0 release in March 2024 meant: stability guarantee, semantic versioning commitment, no more breaking changes to the language design without significant justification. This matters.
Before 1.0, adopting Gleam in production was genuinely risky — the language could change. Post-1.0, that risk is substantially reduced. The API stability means your 1.0-era Gleam code will still compile in Gleam 1.15 (which it does). The language team has been disciplined about this.
The post-1.0 velocity has been impressive: monthly releases, each containing real improvements to developer experience. The language server has gotten significantly better. The JavaScript target performance improved by 30% in v1.11. Code actions, global rename, fault-tolerant compilation — these are professional tooling improvements.
The Honest Bottom Line
Use Gleam if:
- You are starting a new BEAM service and type safety is a priority
- Your team has functional programming experience and will benefit from the type system
- You want to share types between backend (BEAM) and frontend (JS) in a single codebase
- You are willing to invest time in a smaller ecosystem in exchange for a compiler that has your back
Use Elixir if:
- You need Phoenix LiveView
- You need a mature, well-staffed ecosystem with production answers to common problems
- Your team knows Elixir and hiring matters
- You are building on top of existing Elixir/Phoenix infrastructure
Use both if:
- You are introducing type safety gradually into an existing Elixir codebase
- Different services have different requirements (new services in Gleam, existing services staying Elixir)
- You want to evaluate Gleam on a real project before committing
The comparison with Elixir is the relevant one because the target audiences overlap significantly. But it is worth noting that Gleam is not competing with Elixir as much as it is occupying a different point in the type-safety/ecosystem tradeoff. Elixir chose a dynamic type system with optional gradual typing; Gleam chose a static type system from day one. Neither choice is wrong — they reflect different values and different use cases.
Getting Started
The full tour is at tour.gleam.run — interactive, in-browser, covers everything. The cheatsheets at gleam.run/cheatsheets are excellent if you are coming from Elixir, Erlang, Elm, or Python.
|
|
The community is on Discord and the Gleam forum. Both are genuinely welcoming — the core team participates actively and the signal-to-noise ratio is high.
Conclusion
Gleam is the most serious attempt to date to bring ML-family type safety to the BEAM, and as of v1.15 it succeeds. The type system is sound and practical. The use expression is a genuine innovation that solves a real problem. The tooling is excellent. The JavaScript compilation target is well-executed. The Erlang interop story is workable.
The ecosystem is the honest limitation. Gleam is not a drop-in replacement for Elixir in 2026. If you need Phoenix, Ecto, LiveView, or any of the dozens of mature Elixir libraries built over the last decade, you are not getting that from Gleam today. The community is building fast, but “fast” is relative to a small starting point.
What Gleam offers is a language that takes type safety as seriously as Elm or OCaml, running on a runtime that takes fault tolerance as seriously as Erlang, with tooling that takes developer experience as seriously as Go. That combination is novel. For the right use cases — and there are real, important use cases — it is the best tool for the job.
The “if it compiles it mostly works” experience is real. Write a Gleam service, add a new field to a type, watch the compiler enumerate every place that needs updating. Refactor an error type and watch the type system guide you through every call site. It is the kind of feedback loop that makes codebases maintainable over years, not just months.
Gleam 1.0 arrived in 2024. The trajectory since then has been steady and disciplined. It is worth your attention.
Comments