A friendly language for building type-safe systems
gleam v1.x · Erlang & JavaScript targetsThe essential commands to create, build, run, and test Gleam projects.
Scaffold a new Gleam application or library.
gleam new my_app
gleam new my_lib --template lib
Compile and execute the main module.
gleam run
gleam run --target javascript
Execute all test modules in test/.
gleam test
gleam test --target javascript
Compile without running, or check types only.
gleam build
gleam check
Fetch packages from Hex.pm.
gleam add gleam_json
gleam add lustre --dev
Auto-format all .gleam files.
gleam format
gleam format --check
my_app/
// gleam.toml -- project config
// manifest.toml -- locked dependency versions
src/
my_app.gleam // main module
my_app/
router.gleam // submodule: my_app/router
test/
my_app_test.gleam // test module
name = "my_app"
version = "1.0.0"
target = "erlang" // or "javascript"
[dependencies]
gleam_stdlib = "~> 0.44"
gleam_json = "~> 2.0"
[dev-dependencies]
gleeunit = "~> 1.0"
Gleam has full type inference with no null, no exceptions, and no implicit conversions. Every value has a known type at compile time.
| Type | Example | Notes |
|---|---|---|
Int | 42, 1_000_000, 0xff | Arbitrary precision on Erlang, 64-bit float on JS |
Float | 3.14, 1.0e-3 | IEEE 754 double on both targets |
String | "hello" | UTF-8, immutable. No interpolation syntax. |
Bool | True, False | Custom type in the prelude, not a primitive |
Nil | Nil | The unit type, returned by side-effecting functions |
List(a) | [1, 2, 3] | Singly-linked, immutable. All elements same type. |
BitArray | <<1, 2, 3>> | Raw bytes, Erlang binary on BEAM |
Custom types are the backbone of Gleam data modeling. They combine enums, structs, and tagged unions into one construct.
// A simple enum
pub type Direction {
North
South
East
West
}
// A record-like type with fields
pub type User {
User(name: String, email: String, age: Int)
}
// A tagged union (sum type)
pub type Shape {
Circle(radius: Float)
Rectangle(width: Float, height: Float)
Triangle(base: Float, height: Float)
}
Gleam supports parametric polymorphism through type variables. These are lowercase names in type definitions.
// A generic wrapper
pub type Box(value) {
Box(value: value)
}
// A generic binary tree
pub type Tree(a) {
Leaf
Node(value: a, left: Tree(a), right: Tree(a))
}
// Generic function
pub fn identity(x: a) -> a {
x
}
// Multiple type parameters
pub fn pair(a: a, b: b) -> #(a, b) {
#(a, b)
}
Gleam has no exceptions. The Result type is how you represent operations that can fail.
// Result is defined in the prelude as:
pub type Result(value, error) {
Ok(value)
Error(error)
}
// Using Result in a function
pub fn parse_int(input: String) -> Result(Int, String) {
case int.parse(input) {
Ok(n) -> Ok(n)
Error(_) -> Error("Not a valid integer: " <> input)
}
}
// Option is just Result(a, Nil)
pub type Option(a) = Result(a, Nil)
// Give a name to a complex type
pub type Headers = List(#(String, String))
// Aliases are fully interchangeable with the original
pub type UserId = Int
UserId alias does not create a distinct type. A plain Int is accepted wherever UserId is expected. Use a custom type wrapper for true distinction.
Pattern matching is the primary way to branch and destructure data in Gleam. All cases must be exhaustive — the compiler ensures you handle every variant.
pub fn describe(shape: Shape) -> String {
case shape {
Circle(r) -> "Circle with radius " <> float.to_string(r)
Rectangle(w, h) -> "Rectangle " <> float.to_string(w)
<> " x " <> float.to_string(h)
Triangle(b, h) -> "Triangle with base " <> float.to_string(b)
}
}
// Tuple destructuring
let #(x, y, z) = #(1, 2, 3)
// List patterns
case my_list {
[] -> "empty"
[only] -> "one element"
[first, ..rest] -> "head: " <> first
}
// Named field access
let User(name: name, email: email, ..) = user
// Nested patterns
case result {
Ok(User(name: "Lucy", ..)) -> "Found Lucy!"
Ok(user) -> "Found " <> user.name
Error(e) -> "Error: " <> e
}
// Guards with `if`
pub fn classify_age(age: Int) -> String {
case age {
a if a < 0 -> "invalid"
a if a < 13 -> "child"
a if a < 18 -> "teenager"
_ -> "adult"
}
}
// Multiple subjects
case x, y {
0, 0 -> "origin"
0, _ -> "y-axis"
_, 0 -> "x-axis"
_, _ -> "somewhere"
}
// Alternative patterns with `|`
case direction {
North | South -> "vertical"
East | West -> "horizontal"
}
// let-assert panics if the pattern does not match
// Useful in tests or when failure is a bug
let assert Ok(value) = parse_config()
// Regular let requires an irrefutable pattern
let User(name:, ..) = get_user() // Only works if one variant
let assert causes a runtime panic on mismatch. Reserve it for tests and situations where a failed match is truly unrecoverable.
Every .gleam file is a module. Module paths follow the file system.
Access control is simple: pub means public, no keyword means private.
// Import the module, call functions as module.function
import gleam/list
import gleam/string
pub fn main() {
list.map([1, 2, 3], fn(x) { x * 2 })
string.uppercase("hello")
}
// Alias a module
import gleam/string as str
// Unqualified imports (bring names into scope)
import gleam/list.{map, filter, fold}
import gleam/option.{type Option, Some, None}
| Keyword | Applies To | Visibility |
|---|---|---|
pub fn | Functions | Callable from other modules |
fn | Functions | Private to this module |
pub type | Types | Type and constructors are public |
pub opaque type | Types | Type is public, constructors are private |
pub const | Constants | Visible from other modules |
Opaque types expose the type name but hide the constructors, enforcing construction through validated functions.
// In email.gleam
pub opaque type Email {
Email(String)
}
pub fn parse(raw: String) -> Result(Email, String) {
case string.contains(raw, "@") {
True -> Ok(Email(raw))
False -> Error("Invalid email")
}
}
pub fn to_string(email: Email) -> String {
let Email(raw) = email
raw
}
// File: src/my_app/web/router.gleam
// Module: my_app/web/router
import my_app/web/router
import my_app/db/user as user_db
snake_case. Types use PascalCase. Functions and variables use snake_case. Constants use snake_case.
The |> operator passes the result of the left side as the first argument
to the function on the right. It transforms deeply nested calls into readable pipelines.
// Without pipes (nested, hard to read)
string.reverse(string.uppercase(string.trim(" hello ")))
// With pipes (left to right, clear flow)
" hello "
|> string.trim
|> string.uppercase
|> string.reverse
// => "OLLEH"
// The pipe fills the FIRST argument
[1, 2, 3, 4, 5]
|> list.filter(fn(x) { x > 2 })
|> list.map(fn(x) { x * 10 })
|> list.fold(0, int.add)
// => 120
// Equivalent to:
list.fold(list.map(list.filter([1,2,3,4,5], fn(x) { x > 2 }), fn(x) { x * 10 }), 0, int.add)
42
|> int.to_string
|> fn(s) { "The answer is: " <> s }
// Note: this calls the anonymous function with "42"
pub fn handle_request(req: Request) -> Response {
req
|> authenticate
|> result.then(authorize)
|> result.then(validate_body)
|> result.map(process_data)
|> to_response
}
|>.
Gleam uses Result for all fallible operations. No exceptions, no try/catch.
The use expression eliminates callback nesting when chaining Results.
import gleam/result
pub fn get_user_email(id: Int) -> Result(String, String) {
id
|> find_user
|> result.then(validate_active)
|> result.map(fn(user) { user.email })
}
// result.then: (Result(a, e), fn(a) -> Result(b, e)) -> Result(b, e)
// result.map: (Result(a, e), fn(a) -> b) -> Result(b, e)
use is syntactic sugar that flattens callback chains. It rewrites the rest of the block into a callback passed to the right-hand function.
// Without `use` — deeply nested callbacks
pub fn register(data: FormData) -> Result(User, String) {
result.then(validate_name(data.name), fn(name) {
result.then(validate_email(data.email), fn(email) {
result.then(validate_age(data.age), fn(age) {
Ok(User(name:, email:, age:))
})
})
})
}
// With `use` — flat and readable
pub fn register(data: FormData) -> Result(User, String) {
use name <- result.then(validate_name(data.name))
use email <- result.then(validate_email(data.email))
use age <- result.then(validate_age(data.age))
Ok(User(name:, email:, age:))
}
The compiler transforms use x <- f(a) so that everything below it becomes a callback function passed as the last argument to f.
// This:
use x <- result.then(some_result())
do_something(x)
// Becomes:
result.then(some_result(), fn(x) {
do_something(x)
})
use works with any function that takes a callback as its last argument: result.then, list.each, bool.guard, or your own functions.
| Function | Signature | Purpose |
|---|---|---|
result.map | (Result(a, e), fn(a) -> b) -> Result(b, e) | Transform the Ok value |
result.then | (Result(a, e), fn(a) -> Result(b, e)) -> Result(b, e) | Chain fallible operations |
result.map_error | (Result(a, e), fn(e) -> f) -> Result(a, f) | Transform the Error value |
result.unwrap | (Result(a, e), a) -> a | Extract with a default |
result.try_recover | (Result(a, e), fn(e) -> Result(a, f)) -> Result(a, f) | Attempt recovery from error |
result.all | (List(Result(a, e))) -> Result(List(a), e) | Collect all Oks or first Error |
import gleam/bool
pub fn divide(a: Float, b: Float) -> Result(Float, String) {
use <- bool.guard(b == 0.0, Error("Division by zero"))
Ok(a /. b)
}
Gleam compiles to Erlang and runs on the BEAM. You can call any Erlang or Elixir function using the Foreign Function Interface.
Use @external attributes to declare functions implemented in Erlang.
// In your .gleam file, declare the external function
@external(erlang, "erlang", "system_time")
pub fn system_time() -> Int
// Call an Erlang module function
@external(erlang, "crypto", "strong_rand_bytes")
pub fn random_bytes(count: Int) -> BitArray
// Call an Erlang function from a custom module
@external(erlang, "my_app_native", "hash_password")
pub fn hash_password(password: String) -> String
Place Erlang source files alongside your Gleam code. They must share the module name.
%% File: src/my_app/native_ffi.erl
-module(my_app@native_ffi).
-export([hash_password/1]).
hash_password(Password) ->
crypto:hash(sha256, Password).
// File: src/my_app/native_ffi.gleam
@external(erlang, "my_app@native_ffi", "hash_password")
pub fn hash_password(password: String) -> BitArray
@external type signatures. An incorrect signature will compile but crash at runtime. Test FFI boundaries thoroughly.
// Elixir modules are atoms prefixed with "Elixir."
@external(erlang, "Elixir.Jason", "encode!")
pub fn json_encode(data: Dynamic) -> String
@external(erlang, "Elixir.Enum", "sort")
pub fn sort_list(list: List(a)) -> List(a)
When receiving untyped data from Erlang/Elixir, use Dynamic and decoders.
import gleam/dynamic/decode
@external(erlang, "os", "getenv")
fn getenv_ffi(name: String) -> Dynamic
pub fn get_env(name: String) -> Result(String, Nil) {
name
|> getenv_ffi
|> decode.run(decode.string)
|> result.nil_error
}
Gleam can compile to JavaScript, running in Node.js, Deno, Bun, or the browser. The same code can target both Erlang and JavaScript.
// In gleam.toml
target = "javascript"
// Or via CLI flags
gleam run --target javascript
gleam build --target javascript
gleam test --target javascript
Create .mjs files alongside your Gleam modules for JS-specific implementations.
// File: src/my_app/dom_ffi.mjs
export function getElementById(id) {
const el = document.getElementById(id);
if (el === null) return new Error(undefined);
return new Ok(el);
}
export function alert(message) {
window.alert(message);
}
// File: src/my_app/dom_ffi.gleam
@external(javascript, "./dom_ffi.mjs", "getElementById")
pub fn get_element_by_id(id: String) -> Result(Element, Nil)
@external(javascript, "./dom_ffi.mjs", "alert")
pub fn alert(message: String) -> Nil
Provide different implementations for each target using dual @external attributes.
// Erlang implementation
@external(erlang, "erlang", "system_time")
// JavaScript implementation
@external(javascript, "./time_ffi.mjs", "now")
pub fn now() -> Int
// time_ffi.mjs
export function now() {
return Date.now();
}
Lustre is the leading Gleam framework for building web UIs, inspired by Elm.
import lustre
import lustre/element/html
import lustre/event
pub type Msg {
Increment
Decrement
}
fn init(_) -> Int { 0 }
fn update(model: Int, msg: Msg) -> Int {
case msg {
Increment -> model + 1
Decrement -> model - 1
}
}
fn view(model: Int) {
html.div([], [
html.button([event.on_click(Decrement)], [html.text("-")]),
html.p([], [html.text(int.to_string(model))]),
html.button([event.on_click(Increment)], [html.text("+")]),
])
}
pub fn main() {
let app = lustre.simple(init, update, view)
let assert Ok(_) = lustre.start(app, "#app", Nil)
}
On the Erlang target, Gleam has full access to OTP — processes, supervisors, and
message passing. The gleam_otp and gleam_erlang packages provide typed APIs.
Gleam wraps Erlang processes with typed Subject values for safe message passing.
import gleam/erlang/process.{type Subject}
// A Subject(msg) is a typed handle to send messages to a process
// Start a process that receives String messages
pub fn start_logger() -> Subject(String) {
process.new_subject()
}
// Send a typed message
process.send(logger, "Hello from Gleam!")
// Receive with a timeout
let assert Ok(msg) = process.receive(subject, 5000)
The actor module provides a higher-level abstraction over OTP gen_server, with typed state and messages.
import gleam/otp/actor
pub type Msg {
Increment
GetCount(reply_to: Subject(Int))
Shutdown
}
fn handle_message(msg: Msg, count: Int) -> actor.Next(Msg, Int) {
case msg {
Increment -> actor.continue(count + 1)
GetCount(client) -> {
process.send(client, count)
actor.continue(count)
}
Shutdown -> actor.Stop(process.Normal)
}
}
pub fn start_counter() {
actor.start(0, handle_message)
}
import gleam/otp/supervisor
pub fn start() {
supervisor.start(fn(children) {
children
|> supervisor.add(supervisor.worker(fn(_) {
start_counter()
}))
|> supervisor.add(supervisor.worker(fn(_) {
start_logger()
}))
})
}
import gleam/erlang/process.{type Selector}
// A Selector lets a process wait on messages from multiple subjects
let selector =
process.new_selector()
|> process.selecting(subject_a, fn(msg) { FromA(msg) })
|> process.selecting(subject_b, fn(msg) { FromB(msg) })
// Wait for the next message from any source
let assert Ok(msg) = process.select(selector, 5000)
gleam_otp, gleam_erlang) only work on the Erlang target. The JavaScript target has no process model.
Gleam packages are published to Hex.pm and documented on HexDocs. The Gleam compiler handles dependency resolution directly.
| Package | Purpose | Target |
|---|---|---|
gleam_stdlib | Core types, list, string, int, float, result, option, dict | Both |
gleam_json | JSON encoding and decoding | Both |
gleam_http | HTTP types (Request, Response, Method, Header) | Both |
gleam_otp | Actors, supervisors, tasks | Erlang |
gleam_erlang | BEAM process primitives, atoms, ETS | Erlang |
lustre | Elm-style web framework (browser + server components) | Both |
wisp | Web framework for building HTTP servers | Erlang |
mist | HTTP server built on OTP | Erlang |
gleeunit | Simple test runner | Both |
birdie | Snapshot testing | Both |
argv | Command-line argument parsing | Both |
envoy | Environment variable access | Both |
simplifile | File system operations | Both |
// Add a package
gleam add wisp
gleam add gleeunit --dev
// Remove a package
gleam remove wisp
// Update all dependencies to latest compatible versions
gleam update
// Update a specific package
gleam update gleam_json
// View dependency tree
gleam deps list
// Ensure gleam.toml has required fields
name = "my_package"
version = "1.0.0"
description = "A useful Gleam library"
licences = ["Apache-2.0"]
[repository]
type = "github"
user = "your-name"
repo = "my_package"
// Publish to hex.pm
gleam publish
// Generate documentation
gleam docs build
gleam docs publish
Here is a minimal Gleam web application using Wisp and Mist, showing how the ecosystem fits together.
import gleam/bytes_tree
import gleam/http/response
import mist
import wisp
import wisp/wisp_mist
pub fn main() {
let secret_key = wisp.random_string(64)
let handler = fn(req) {
use req <- wisp_mist.handler(req, secret_key)
handle_request(req)
}
let assert Ok(_) =
handler
|> mist.new
|> mist.port(8000)
|> mist.start_http
process.sleep_forever()
}
fn handle_request(req: wisp.Request) -> wisp.Response {
case wisp.path_segments(req) {
[] -> wisp.ok()
|> wisp.string_body("Hello from Gleam!")
["api", "health"] -> wisp.ok()
|> wisp.json_body("{\"status\": \"ok\"}")
_ -> wisp.not_found()
}
}
// test/my_app_test.gleam
import gleeunit
import gleeunit/should
import my_app
pub fn main() {
gleeunit.main()
}
pub fn add_test() {
my_app.add(2, 3)
|> should.equal(5)
}
pub fn parse_email_test() {
my_app.parse_email("lucy@example.com")
|> should.be_ok
my_app.parse_email("not-an-email")
|> should.be_error
}