Gleam for Erlang Developers: Type-Safe Language for the BEAM VM

Gleam is a statically-typed functional language that compiles to Erlang BEAM bytecode and JavaScript. It gives you OTP’s fault tolerance and distribution with Hindley-Milner type inference - the same type system family as Haskell and OCaml - without making you leave the BEAM ecosystem you already know. As of April 2026, the latest stable release is v1.15.3, and the ecosystem has matured to include a full HTTP server stack (Wisp + Mist ), database drivers, and a built-in language server. If you write Erlang or Elixir professionally, Gleam is worth your attention.

Why the BEAM Needed a Typed Language

The BEAM VM is exceptional at what it does. Preemptive scheduling across lightweight processes, fault tolerance via supervision trees, hot code reloading, and the actor model - these properties are why telecom systems, messaging platforms, and financial services chose Erlang in the first place. Gleam inherits all of this for free because it compiles to BEAM bytecode. Your Gleam code runs on the same VM, uses the same schedulers, and crashes the same way (in a good, supervised way).

But Erlang and Elixir’s dynamic typing creates friction that grows with codebase size. Refactoring a large Elixir application means running the test suite and hoping for the best. Argument-order bugs in function calls compile fine and only surface at runtime. Integrating third-party libraries means reading docs carefully and trusting that the types match what you expect, because the compiler won’t check.

The BEAM community has tried to address this. Dialyzer offers opt-in type analysis, but it’s slow, produces false positives, and many teams skip it. Elixir is working on set-theoretic types, a gradual typing approach that should improve things incrementally. Gleam takes a different path: full static type inference from the start. Every function has a known type signature, and the compiler rejects programs with type mismatches before they run.

Gleam chose full inference over gradual typing because it makes the type system useful from day one. You don’t annotate most types - the compiler figures them out - but every function boundary becomes a contract. The ML-family type system catches real bugs (wrong argument order, missing case branches, nil where a value was expected) without the annotation fatigue that plagues languages like Java.

The language hit 1.0 in March 2024. Two years later, the ecosystem has caught up. In the 2025 Stack Overflow Developer Survey, Gleam appeared for the first time and ranked as the 2nd “most admired” language. Thoughtworks placed it in the Assess ring of their Technology Radar in April 2025. The community is small but growing fast.

Lucy, the Gleam programming language mascot - a friendly pink star character
Gleam's mascot Lucy, representing the language's approachable design philosophy
Image: Wikimedia Commons , Apache License 2.0

Gleam Syntax for Erlang and Elixir Developers

If you know Erlang or Elixir, you already understand pattern matching, tagged tuples, and immutability. Gleam’s syntax will still surprise you, though - it’s closer to Rust than to either of its BEAM siblings.

A simple function looks like this:

pub fn greet(name: String) -> String {
  "Hello, " <> name
}

Curly braces instead of do...end. Explicit type annotations on function signatures (though the compiler can infer them). String concatenation with <> rather than interpolation.

Pattern matching uses case expressions, and the compiler enforces exhaustiveness. If you add a variant to a type and forget to handle it somewhere, the build fails:

case user.role {
  Admin -> "Full access"
  Editor -> "Can edit"
  Viewer -> "Read only"
}

Types are defined with pub type. You get sum types (discriminated unions), records, opaque types, and generics:

pub type User {
  User(name: String, role: Role, active: Bool)
}

pub type Role {
  Admin
  Editor
  Viewer
}

One of the biggest adjustments for Elixir developers: Gleam replaces nil and the {:ok, value} | {:error, reason} tuple convention with proper Result and Option types. The compiler forces you to handle both cases, so “forgot to check for nil” bugs stop happening.

The pipe operator |> works exactly like Elixir’s version, but with a key difference - the compiler type-checks every stage of the pipeline:

request
|> parse_body
|> validate_input
|> save_to_database
|> format_response

If parse_body returns a Result(Body, Error) but validate_input expects a plain Body, the compiler tells you immediately.

Building an HTTP API With Wisp and Mist

Most developers reaching for Gleam want to build an HTTP service first. Wisp is the practical web framework; Mist is the HTTP server that runs behind it.

Start a new project and add dependencies:

gleam new my_api
cd my_api
gleam add wisp mist gleam_http gleam_json

The gleam.toml manifest tracks dependencies, which are pulled from hex.pm - the same package registry Elixir uses. No separate ecosystem.

A basic request handler is a function with a typed signature:

import wisp.{type Request, type Response}

pub fn handle_request(req: Request) -> Response {
  case wisp.path_segments(req) {
    ["api", "users"] -> list_users(req)
    ["api", "users", id] -> get_user(req, id)
    _ -> wisp.not_found()
  }
}

Routing is pattern matching on path segments. No route DSL to learn, no macros - just a case expression that the compiler type-checks.

For JSON handling, gleam_json provides decode/encode functions that integrate with Gleam’s Result type. A malformed request body produces a Result error that the compiler forces you to handle:

import gleam/json

pub fn create_user(req: Request) -> Response {
  use body <- wisp.require_json(req)
  case decode_user(body) {
    Ok(user) -> {
      // save user, return 201
      wisp.json_response(encode_user(user), 201)
    }
    Error(_) -> wisp.bad_request()
  }
}

The use keyword in Wisp handles middleware composition. It works like Go’s deferred execution but at the expression level - you can stack middleware without deep nesting:

pub fn middleware(req: Request, handler: fn(Request) -> Response) -> Response {
  use <- wisp.log_request(req)
  use <- wisp.rescue_crashes
  use req <- wisp.handle_head(req)

  handler(req)
}

To run the server:

import mist

pub fn main() {
  let handler = middleware(_, handle_request)

  mist.new(handler)
  |> mist.port(8080)
  |> mist.start_http
}

For production deployment, Gleam can produce an Erlang release - a self-contained package with the BEAM runtime included:

gleam export erlang-shipment

This produces a build/erlang-shipment directory you can copy to any Linux server and run without installing Erlang separately.

Interop With Existing Erlang and Elixir Code

Gleam’s practical value depends on using the BEAM ecosystem you’ve already built around. Interop works well enough for production use.

The FFI uses @external declarations. You write the type signature in Gleam, and the compiler type-checks all callers even though the implementation lives in Erlang or Elixir:

@external(erlang, "crypto", "hash")
pub fn hash(algorithm: Atom, data: BitArray) -> BitArray

Gleam pulls packages from hex.pm directly. An Erlang library like cowlib or an Elixir library like jason can be added with gleam add and called through FFI bindings. There’s no ecosystem split.

For OTP behaviors, the gleam_otp package provides typed wrappers around gen_server, supervisor, and task. The actor model gets proper mailbox typing:

import gleam/otp/actor

pub fn start_counter() {
  actor.start(0, fn(message, count) {
    case message {
      Increment -> actor.continue(count + 1)
      GetCount(reply) -> {
        actor.send(reply, count)
        actor.continue(count)
      }
    }
  })
}

The message type is inferred from usage. If you try to send a message type that the actor doesn’t handle, the compiler catches it.

You can also embed Gleam modules inside an existing Elixir Phoenix project. Gleam compiles into the _build directory in BEAM bytecode format, and Elixir can call those modules directly. This lets you adopt Gleam one module at a time rather than rewriting everything.

For debugging mixed-language stacks, the standard BEAM tools work fine. Observer shows process trees, message queues, and memory usage regardless of which language spawned the process.

The JavaScript Backend and Full-Stack Gleam

Gleam doesn’t only compile to BEAM bytecode. It also targets JavaScript, and since late 2025, the JS output has gotten 30% faster thanks to compiler optimizations.

This means you can write shared types and validation logic once and compile them to both Erlang (for your backend) and JavaScript (for your frontend). The Lustre framework takes this further - it’s an Elm-inspired UI framework written in Gleam that compiles to JavaScript for the browser.

Gleam dual-target compilation pipeline showing how Gleam source compiles through the Rust-based compiler to both Erlang source and JavaScript output

A full-stack Gleam application shares data types between server and client:

// This type compiles to both Erlang and JavaScript
pub type TodoItem {
  TodoItem(id: String, text: String, completed: Bool)
}

The same validation function runs on both targets. No need to maintain parallel TypeScript types and Elixir structs that might drift apart.

Gleam also generates TypeScript type definitions when compiling to JS, so if your frontend is partially TypeScript, you get type safety at the boundary. Developers coming from the JavaScript ecosystem may want to compare it with Hono, a fast TypeScript web framework that runs on Bun, Deno, Cloudflare Workers, and Node.js with a single codebase.

Testing in Gleam

New Gleam projects come with gleeunit as the default test runner. Tests are functions with names ending in _test:

import gleeunit/should

pub fn greet_test() {
  greet("World")
  |> should.equal("Hello, World")
}

Run them with gleam test. On the Erlang target, gleeunit uses EUnit under the hood. On JavaScript, it provides its own runner.

For property-based testing, gleam_qcheck offers QuickCheck-style random input generation with integrated shrinking. It’s modeled after OCaml’s qcheck library, which fits naturally with Gleam’s ML-family type system.

Two other test tools are useful: Glacier provides incremental test watching (re-runs only affected tests on file save), and birdie offers snapshot testing for regression detection.

Production Deployment and Ecosystem Maturity

Gleam ships as a single binary that includes the compiler, build tool, formatter, test runner, documentation generator, and package manager. Run gleam lsp and you get a full Language Server Protocol implementation that works with VS Code, Neovim, Helix, and Zed.

Diagram showing how the Language Server Protocol reduces integration complexity from N times M to N plus M connections between languages and editors
The LSP approach lets Gleam support multiple editors through a single language server implementation
Image: Gleam Blog

For deployment, the typical path is building an Erlang release with gleam export erlang-shipment and deploying it to Fly.io , Render, or a traditional VPS running under systemd. The release includes the BEAM runtime, so the target machine needs no language-specific tooling.

Observability options exist through Elixir interop. OpenTelemetry integration works via FFI to Elixir’s opentelemetry packages. Structured logging goes through gleam/io or FFI to Erlang’s logger. Metrics can be exported via prom_ex (an Elixir library) called through FFI.

What’s Solid and What’s Missing

AreaStatusNotes
HTTP server (Wisp + Mist)MatureProduction-ready, well-documented
Database queriesGrowingCake (query builder) + sqlight for SQLite, PostgreSQL via FFI
Database migrationsImmatureNo Ecto-equivalent; use Elixir’s Ecto via FFI or manual SQL
JSON handlingMaturegleam_json with typed encode/decode
Editor toolingMatureBuilt-in LSP with full features
HTTP clientGrowingFewer options than Elixir; gleam_httpc covers basics
Community sizeSmallGrowing fast, but far fewer tutorials and Stack Overflow answers than Elixir
Frontend (Lustre)GrowingElm-style architecture, active development

Gleam’s honest weakness right now is database migration tooling. If you’re used to Ecto’s migration DSL and schema management, you’ll feel the gap. The workaround is calling Ecto through FFI from a mixed project, but that defeats some of the purpose of choosing Gleam.

The other gap is community size. Elixir has years of blog posts, conference talks, and production war stories. Gleam’s community is enthusiastic but small. When you hit an edge case, you may find yourself reading source code rather than Stack Overflow answers.

When to Pick Gleam Over Elixir or Erlang

CriterionChoose GleamChoose ElixirChoose Erlang
Type safety priorityStrong static types neededGradual typing acceptableDialyzer sufficient
Team backgroundML/Rust/Haskell developersRuby/Python developersTelecom/embedded experience
Codebase sizeLarge, multi-teamMedium to largeAny size
Database needsSimple queries, or FFI to EctoFull ORM with migrationsRaw SQL or Mnesia
Frontend sharingLustre + JS backendLiveViewNot applicable
Ecosystem maturityWilling to fill gapsNeed batteries includedNeed battle-tested libs

Gleam occupies a different spot than Elixir or Erlang on the same platform. If your team values compile-time guarantees over ecosystem breadth, and you’re comfortable with a smaller community, Gleam gives you real type safety on the best concurrent runtime available.

The language is at v1.15.3 with no announced plans for a 2.0. The roadmap includes function inlining for performance, better JavaScript output, and improved OTP app initialization. The focus is on polishing the 1.x series rather than breaking changes - which is exactly what you want from a language you’re considering for production.