Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Language Atlas (mini-courses for more languages)  Module 10 – Gleam on the BEAM and in JavaScript

Gleam for JavaScript and Python developers

Map JavaScript and Python habits to Gleam: custom types and case, Result and use instead of exceptions, pipes and labels, and integers that differ by target.

  • Beginner
  • 20 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0, Gleam 1.18.1 and Erlang/OTP 29.1.1
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Model data with custom types and case expressions, and let the compiler list every case a new variant breaks
  • Handle expected failures with Result and use, and tell when let assert is the right tool
  • Write pipelines with labelled arguments and inspect them with echo
  • Predict integer division, rounding and parsing in Gleam, and compare them with Python and JavaScript
  • Recognise code that builds for one target only and keep it out of shared modules

Before you start

What you’ll learn · Before you start

What you will learn

  • Model data with custom types and case expressions, and let the compiler list every case a new variant breaks
  • Handle expected failures with Result and use, and tell when let assert is the right tool
  • Write pipelines with labelled arguments and inspect them with echo
  • Predict integer division, rounding and parsing in Gleam, and compare them with Python and JavaScript
  • Recognise code that builds for one target only and keep it out of shared modules

Before you start

On this page

A form check written in JavaScript accepted "4e1" as an age, because Number("4e1") is 40, and reported only the first of three problems, because it throws. In Gleam the same check returns every problem as a value, and int.parse("4e1") gives Error(Nil): Gleam has no null, no exceptions for expected failures and no implicit conversions, so the habits that carry those bugs have nowhere to go.

Here you map what you already write in JavaScript or Python to Gleam: custom types and case instead of classes, enums and null; Result and use instead of exceptions; pipelines with labelled arguments, read with echo; whole numbers that divide, round and parse unlike both languages you know; and the line between code that runs on both targets and code that needs the BEAM. Every program here ran on Erlang/OTP 29.1.1 and on Node.js 24.21.0.

Custom types and case replace classes, enums and null

Where JavaScript has a class hierarchy or an object with a kind field, and Python has subclasses or a match over dataclasses, Gleam has one custom type: A Gleam type defined with pub type and one or more variants, each with its own fields, such as Shape with Circle, Rectangle and Square. A value of the type is always exactly one of its variants. with several variants, each with its own fields. A Shape is a Circle, a Rectangle or a Square, and nothing else: there is no null shape and no fourth kind that code forgets. A case expression takes a value apart by its variant:

shapes.gleam: one type, three variants, two case expressions GLEAM · basics/src/shapes.gleam
import gleam/float
import gleam/io
import gleam/list

/// π, as close as a Float can hold it.
const pi = 3.141592653589793

/// A shape is one of three variants, each with its own fields.
pub type Shape {
  Circle(radius: Float)
  Rectangle(width: Float, height: Float)
  Square(side: Float)
}

pub fn area(shape: Shape) -> Float {
  case shape {
    Circle(radius:) -> pi *. radius *. radius
    Rectangle(width:, height:) -> width *. height
    Square(side:) -> side *. side
  }
}

pub fn name(shape: Shape) -> String {
  case shape {
    Circle(..) -> "circle"
    Rectangle(width:, height:) if width == height -> "rectangle with equal sides"
    Rectangle(..) -> "rectangle"
    Square(..) -> "square"
  }
}

pub fn main() -> Nil {
  [
    Circle(radius: 2.0),
    Rectangle(width: 2.0, height: 3.5),
    Rectangle(4.0, 4.0),
    Square(side: 1.5),
  ]
  |> list.each(fn(shape) {
    io.println(name(shape) <> ": area " <> float.to_string(area(shape)))
  })
}

Circle(radius:) matches any circle and binds its field to radius. Rectangle(..) matches any rectangle and ignores its fields. A guard, if width == height, adds a condition to a pattern. Step through the case that names a Rectangle(4.0, 4.0), and then the one change that would break it:

Interactive · stage by stage How case picks a branch for Rectangle(4.0, 4.0) Step through name(Rectangle(4.0, 4.0)) pattern by pattern, then see what the compiler does with Triangle.
How case picks a branch for Rectangle(4.0, 4.0), step 6 of 6. Showing Rectangle(4.0, 4.0), Circle(..), Rectangle, if equal, Rectangle(..), Square(..), The result. In focus: missing.no pattern fitsRectangle(4.0, 4.0)the value of shapeCircle(..)any circleRectangle, if equalwidth == heightRectangle(..)any other rectangleSquare(..)any squareThe resultone String

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. The value. name receives Rectangle(width: 4.0, height: 4.0). A case expression tries its patterns from the top and runs the first one that matches.
  2. Not a circle. Circle(..) matches any Circle, whatever its radius, and only a Circle. This value was built with Rectangle, so the next pattern is tried.
  3. A guard. Rectangle(width:, height:) matches and binds both fields, and the guard if width == height holds for 4.0 and 4.0, so this branch runs.
  4. Branches not tried. The two patterns below are never tried for this value. Rectangle(2.0, 3.5) would fail the guard and stop at Rectangle(..), the next line.
  5. One result. The case expression is a value: here the string rectangle with equal sides, which main printed with the area 16.0.
  6. A variant without a pattern. Add Triangle to Shape and keep the case as it is: no pattern would fit a Triangle. The compiler refuses to build and names the missing pattern, Triangle(base:, height:), for each case expression.

The session runs the module on both targets, then adds a fourth variant, Triangle, without touching the two case expressions:

shapes on both targets, then a Triangle that no case handles Python · types_session.py
from session import run

# Custom types and case: shapes.gleam on both targets, then the compiler's answer when a fourth variant, Triangle,
# is added (triangle/shapes.gleam) without updating the two case expressions that read a Shape.
run(
    """
cd basics
gleam run -m shapes
gleam run -m shapes --target javascript
cp ../triangle/shapes.gleam src/shapes.gleam
!gleam build
"""
)

Output

$ cd basics
$ gleam run -m shapes
Downloading packages
 Downloaded 2 packages in …s
  Compiling gleam_stdlib
  Compiling gleeunit
  Compiling basics
   Compiled in …s
    Running shapes.main
circle: area 12.566370614359172
rectangle: area 7.0
rectangle with equal sides: area 16.0
square: area 2.25
$ gleam run -m shapes --target javascript
  Compiling gleam_stdlib
  Compiling gleeunit
  Compiling basics
   Compiled in …s
    Running shapes.main
circle: area 12.566370614359172
rectangle: area 7.0
rectangle with equal sides: area 16.0
square: area 2.25
$ cp ../triangle/shapes.gleam src/shapes.gleam
$ gleam build
  Compiling basics
error: Inexhaustive patterns
   ┌─ src/shapes.gleam:17:3
   │
17 │ ╭   case shape {
18 │ │     Circle(radius:) -> pi *. radius *. radius
19 │ │     Rectangle(width:, height:) -> width *. height
20 │ │     Square(side:) -> side *. side
21 │ │   }
   │ ╰───^

This case expression does not have a pattern for all possible values. If it
is run on one of the values without a pattern then it will crash.

The missing patterns are:

    Triangle(base:, height:)

error: Inexhaustive patterns
   ┌─ src/shapes.gleam:25:3
   │
25 │ ╭   case shape {
26 │ │     Circle(..) -> "circle"
27 │ │     Rectangle(width:, height:) if width == height -> "rectangle with equal sides"
28 │ │     Rectangle(..) -> "rectangle"
29 │ │     Square(..) -> "square"
30 │ │   }
   │ ╰───^

This case expression does not have a pattern for all possible values. If it
is run on one of the values without a pattern then it will crash.

The missing patterns are:

    Triangle(base:, height:)
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 types_session.py

Both targets printed the same four lines. With Triangle added, the build stopped with Inexhaustive patterns at both case expressions, and each error lists Triangle(base:, height:) as the missing pattern. That is the compiler’s exhaustiveness check: The compiler's check that the patterns of a case expression cover every possible value. In Gleam a case that misses a variant does not compile: the error, Inexhaustive patterns, lists the missing patterns.: a variant added to a type is a list of compile errors at every case that must learn about it, not a TypeError in production. Where JavaScript returns undefined and Python returns None, Gleam returns a Result (the standard library’s list.first does) or an Option from gleam/option, and the type says so.

A catch-all pattern that hides the next variant

Ending a case with _ -> "unknown shape" silences the exhaustiveness check: when Triangle arrives, it is quietly called unknown. Name every variant, as shapes.gleam does, so that the compiler can point at each case to update.

Checkpoint

Check: Custom types and case

  1. Question 1 of 2 You add Triangle(base: Float, height: Float) to Shape and change nothing else. What happens when you build?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The build fails with Inexhaustive patterns at each case on a Shape, naming Triangle(base:, height:)

    The exhaustiveness check turns a new variant into a list of compile errors at every case that must handle it, so nothing reaches production with a forgotten variant.

  2. Question 2 of 2 What does shapes.name return for Rectangle(width: 2.0, height: 3.5)?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: "rectangle"

    A case tries its patterns from the top. A guard adds a condition; when it fails, the next pattern is tried, as for Rectangle(2.0, 3.5), which main printed as rectangle with area 7.0.

Expected failures are Result values, and use keeps the happy path flat

The language tour puts it plainly: Gleam doesn’t use exceptions, and computations that can succeed or fail return the built-in Result: Gleam's built-in type for a computation that can fail: Ok(value) when it worked, Error(reason) when it did not. Result(Int, String) holds an Int or a String reason, and the caller must handle both., Ok(value) or Error(reason). parse_age returns Result(Int, String), so every caller sees in the type that it can fail. parse_person needs two such steps, the split at : and the age, and a use expression: Gleam syntax, use x <- f(a), that turns all the code below it into an anonymous function fn(x) { … } passed as the last argument of f. With result.try it carries on with an Ok value or returns the Error at once. with result.try writes each step on one line:

results.gleam: parse_age and parse_person GLEAM · basics/src/results.gleam
import gleam/int
import gleam/list
import gleam/result
import gleam/string

pub type Person {
  Person(name: String, age: Int)
}

/// An age from text, or the reason there is none.
pub fn parse_age(text: String) -> Result(Int, String) {
  case int.parse(string.trim(text)) {
    Ok(age) if age >= 0 && age <= 150 -> Ok(age)
    Ok(age) -> Error("age out of range: " <> int.to_string(age))
    Error(Nil) -> Error("not a number: " <> text)
  }
}

/// "name:age" to a Person. Each `use` line returns the error at once, or
/// carries on with the value inside Ok.
pub fn parse_person(line: String) -> Result(Person, String) {
  use #(name, age_text) <- result.try(
    string.split_once(line, ":")
    |> result.replace_error("no colon in: " <> line),
  )
  use age <- result.try(parse_age(age_text))
  Ok(Person(name: string.trim(name), age:))
}

pub fn main() -> Nil {
  ["asha:31", "ravi:abc", "meera", "joe:200"]
  |> list.each(fn(line) { echo parse_person(line) })
}

The tour describes use exactly: all the code below a use line becomes an anonymous function, passed as the last argument of the call on the right of <-. result.try calls that function with the value inside Ok, or returns the Error at once. Follow parse_person("ravi:abc") through both lines:

Interactive · message by message What the two use lines do with "ravi:abc" Step through parse_person("ravi:abc") call by call; the last steps show the line that does succeed.
What the two use lines do with "ravi:abc", step 6 of 6. Messages so far: parse_person to string.split_once: "ravi:abc"; string.split_once to parse_person: Ok(#("ravi", "abc")); parse_person to parse_age: "abc"; parse_age to parse_person: Error("not a number: abc"); parse_age to parse_person: Ok(31).parse_persontwo use linesstring.split_onceat the first :parse_ageint.parse, 0 to 150"ravi:abc"Ok(#("ravi", "abc"))"abc"Error("not a number: abc")Ok(31)

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Split the line. The first use line calls string.split_once on "ravi:abc". result.try receives its Result and the rest of the function, written below the use line, as a callback.
  2. Ok, carry on. split_once returns Ok(#("ravi", "abc")). result.try calls the callback with the pair, so name is "ravi" and age_text is "abc".
  3. Parse the age. The second use line calls parse_age("abc"). int.parse cannot read "abc" as a whole number and returns Error(Nil).
  4. An Error stops here. parse_age turns that into Error("not a number: abc"). result.try returns this Error at once, without calling the rest of the function.
  5. No exception, a value. parse_person returns the Error to its caller as an ordinary value, and its type, Result(Person, String), told the caller to expect one.
  6. The line that succeeds. For "asha:31" both calls return Ok, so the last line runs and builds Person(name: "asha", age: 31). Echo printed it as Ok(Person("asha", 31)) on Erlang.

crash.gleam shows the other tool, let assert: A Gleam assignment whose pattern may not match, such as let assert Ok(age) = parse_age(text). When it does not match, the program crashes on purpose with an optional message, so it is kept for values that cannot be wrong., for a value the program has already checked and cannot use without: a pattern that may not match, and a deliberate crash with your message when it does not. The session runs both modules on both targets:

crash.gleam: let assert on a value that was not checked GLEAM · basics/src/crash.gleam
import gleam/io
import results

pub fn main() -> Nil {
  let assert Ok(first) = results.parse_age("31")
  io.println("parsed 31")
  let assert Ok(second) = results.parse_age("abc")
    as "the form should have been checked first"
  io.println("never printed")
  echo first + second
  Nil
}
parse_person on four lines, then let assert, on both targets Python · results_session.py
from session import run

# Results instead of exceptions: parse_person on four lines, on both targets (echo prints each value with its file
# and line), then let assert, which crashes on purpose when its pattern does not match.
run(
    """
cd basics
gleam run -m results
gleam run -m results --target javascript
!gleam run -m crash
!gleam run -m crash --target javascript
"""
)

Output

$ cd basics
$ gleam run -m results
Downloading packages
 Downloaded 2 packages in …s
  Compiling gleam_stdlib
  Compiling gleeunit
  Compiling basics
   Compiled in …s
    Running results.main
src/results.gleam:32
Ok(Person("asha", 31))
src/results.gleam:32
Error("not a number: abc")
src/results.gleam:32
Error("no colon in: meera")
src/results.gleam:32
Error("age out of range: 200")
$ gleam run -m results --target javascript
  Compiling gleam_stdlib
  Compiling gleeunit
  Compiling basics
   Compiled in …s
    Running results.main
src/results.gleam:32
Ok(Person(name: "asha", age: 31))
src/results.gleam:32
Error("not a number: abc")
src/results.gleam:32
Error("no colon in: meera")
src/results.gleam:32
Error("age out of range: 200")
$ gleam run -m crash
   Compiled in …s
    Running crash.main
parsed 31
runtime error: let assert

the form should have been checked first

unmatched value:
  Error("not a number: abc")

stacktrace:
  crash.main src/crash.gleam:7
[exit status 1]
$ gleam run -m crash --target javascript
   Compiled in …s
    Running crash.main
parsed 31
build/dev/javascript/prelude.mjs:1595
  let error = new globalThis.Error(message);
              ^

Error: the form should have been checked first
    at makeError (build/dev/javascript/prelude.mjs:1595:15)
    at main (build/dev/javascript/basics/crash.mjs:41:11)
    at build/dev/javascript/basics/gleam@@private_main_v1.18.1.mjs:2:1
    at ModuleJob.run (node:internal/modules/esm/module_job:561:25)
    at async node:internal/modules/esm/loader:647:26
    at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5) {
  gleam_error: 'let_assert',
  file: 'src/crash.gleam',
  module: 'crash',
  line: 7,
  function: 'main',
  fn: 'main',
  value: Error { '0': 'not a number: abc' },
  start: 132,
  end: 180,
  pattern_start: 143,
  pattern_end: 153
}

Node.js v24.21.0
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 results_session.py

The four lines gave one Ok and three different Errors, and nothing was thrown. Look closely at the Ok: on the BEAM echo printed Ok(Person("asha", 31)), and on Node.js Ok(Person(name: "asha", age: 31)), so debug output can differ between targets, while the values are equal. let assert stopped the program with exit status 1 on both: the BEAM printed runtime error: let assert, the message and the unmatched value; Node.js threw an Error whose fields carry the same message, file and line.

Key idea

A failure the caller should handle is an Error in the return type, and use … <- result.try(…) keeps a chain of such steps flat; let assert is only for values that cannot be wrong, and it crashes with your message when they are.

Checkpoint

Check: Result, use and let assert

  1. Question 1 of 2 parse_person("joe:200") runs. Which line of parse_person produces its result?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The second use line: parse_age("200") returns Error("age out of range: 200"), and result.try returns it at once

    Each use line either carries on with the value inside Ok or returns the Error at once. The session printed Error("age out of range: 200") for this line.

  2. Question 2 of 2 A request handler does let assert Ok(age) = int.parse(form_age) on text a visitor typed. What is wrong with it?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A visitor who types "abc" crashes the handler: let assert is for values that cannot be wrong, and input can be

    Use let assert only where a failure means a bug in your own code. Text from outside is checked with a case or a use line, so a bad value becomes an error message, not a crash.

Pipes, labels and echo read in the order the data flows

f(g(h(x))) reads inside out. A Gleam pipeline reads the way the data moves: x |> h |> g |> f, where each |> passes the value on its left as the first argument of the call on its right. JavaScript has no pipe operator yet, and Python has none, so this is the line where both groups slow down at first. pipes.gleam turns a messy list of names into one line, and computes an order total with labelled argument: A function argument with a name callers write, such as discount: 10. A Gleam declaration gives each a label and an internal name, discount percent: Int, and labelled arguments may be passed in any order.:

pipes.gleam: a pipeline, echo inside it, and labelled arguments GLEAM · basics/src/pipes.gleam
import gleam/int
import gleam/io
import gleam/list
import gleam/string

/// `discount` and `delivery` are labels: a call names them, in any order.
pub fn total(
  prices: List(Int),
  discount percent: Int,
  delivery fee: Int,
) -> Int {
  let sum = int.sum(prices)
  sum - sum * percent / 100 + fee
}

pub fn main() -> Nil {
  " Meera, asha ,Ravi "
  |> string.split(",")
  |> list.map(string.trim)
  |> echo
  |> list.map(string.capitalise)
  |> list.sort(string.compare)
  |> string.join(with: " · ")
  |> io.println

  let a = total([450, 120, 30], discount: 10, delivery: 40)
  let b = total([450, 120, 30], delivery: 40, discount: 10)
  io.println("total: " <> int.to_string(a) <> " and " <> int.to_string(b))
}

Step through the pipeline, with the value after each step:

Interactive · stage by stage The names pipeline, one step at a time Step through the pipeline from the text to the printed line; each sub-label is the value at that point.
The names pipeline, one step at a time, step 7 of 7. Showing The text, string.split(","), list.map(string.trim), echo, list.map(capitalise), list.sort, string.join(with:). In focus: string.join(with:).The text" Meera, asha ,Ravi "string.split(",")three untrimmed nameslist.map(string.trim)["Meera", "asha", "Ravi"]echoprints it, passes it onlist.map(capitalise)["Meera", "Asha", "Ravi"]list.sort["Asha", "Meera", "Ravi"]string.join(with:)"Asha · Meera · Ravi"

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. Start with the text. The pipeline starts from one string with spaces around the commas. Each |> passes the value on its left as the first argument of the call on its right.
  2. Split. string.split(",") gives a list of three strings, still with their spaces: " Meera", " asha " and "Ravi ".
  3. Trim each. list.map(string.trim) calls trim on each name. string.trim is passed as a function, without calling it.
  4. Look, without stopping. echo prints the value it is given, after the file and line, here src/pipes.gleam:20, and returns it unchanged, so the pipeline goes on.
  5. Capitalise. list.map(string.capitalise) turns asha into Asha. The other two names already start with a capital letter.
  6. Sort. list.sort(string.compare) sorts the names. Gleam has no default ordering, so the comparison function is passed in.
  7. Join with a label. string.join(with: " · ") names its second argument by its label, with. io.println then printed Asha · Meera · Ravi.

echo in the middle of the pipeline printed src/pipes.gleam:20 and the list to standard error, then returned the list, so nothing else changed. It is for debugging only: the changelog of Gleam 1.9, which added it, says the build tool refuses to publish a package that still has echo in it. total declares discount percent and delivery fee: the first word is the label callers use, the second the name inside the function, so total(prices, discount: 10, delivery: 40) and total(prices, delivery: 40, discount: 10) are the same call. The session also runs form.gleam, the port of the form check, and the module’s five tests on both targets:

form.gleam: every problem of a form, as one Result GLEAM · basics/src/form.gleam
import gleam/int
import gleam/list
import gleam/string

pub type Form {
  Form(name: String, age: Int, email: String)
}

/// Every problem of the three fields, in field order, or the checked form.
pub fn validate(
  name name: String,
  age age: String,
  email email: String,
) -> Result(Form, List(String)) {
  let name = string.trim(name)
  let checked_name = case name {
    "" -> Error("name is required")
    _ -> Ok(name)
  }
  let checked_age = case int.parse(string.trim(age)) {
    Error(Nil) -> Error("age must be a whole number")
    Ok(years) if years < 18 || years > 120 ->
      Error("age must be between 18 and 120")
    Ok(years) -> Ok(years)
  }
  let email = string.trim(email)
  let checked_email = case string.split(email, "@") {
    [user, domain] if user != "" && domain != "" -> Ok(email)
    _ -> Error("email needs one @ with text on both sides")
  }
  case checked_name, checked_age, checked_email {
    Ok(name), Ok(age), Ok(email) -> Ok(Form(name:, age:, email:))
    _, _, _ ->
      Error(
        list.filter_map(
          [problem(checked_name), problem(checked_age), problem(checked_email)],
          fn(found) { found },
        ),
      )
  }
}

fn problem(checked: Result(a, String)) -> Result(String, Nil) {
  case checked {
    Ok(_) -> Error(Nil)
    Error(message) -> Ok(message)
  }
}

pub fn main() -> Nil {
  let _ = echo validate(name: " Asha ", age: "31", email: "asha@example.com")
  let _ = echo validate(name: "", age: "4e1", email: "ravi.example.com")
  let _ = echo validate(name: "Meera", age: "12", email: "meera@example.com")
  Nil
}
pipes and form, then the tests on both targets Python · pipes_session.py
from session import run

# Pipes and labelled arguments, a form check that collects every problem, and the module's tests on both targets.
run(
    """
cd basics
gleam run -m pipes
gleam run -m form
gleam test
gleam test --target javascript
"""
)

Output

$ cd basics
$ gleam run -m pipes
Downloading packages
 Downloaded 2 packages in …s
  Compiling gleam_stdlib
  Compiling gleeunit
  Compiling basics
   Compiled in …s
    Running pipes.main
src/pipes.gleam:20
["Meera", "asha", "Ravi"]
Asha · Meera · Ravi
total: 580 and 580
$ gleam run -m form
   Compiled in …s
    Running form.main
src/form.gleam:51
Ok(Form("Asha", 31, "asha@example.com"))
src/form.gleam:52
Error(["name is required", "age must be a whole number", "email needs one @ with text on both sides"])
src/form.gleam:53
Error(["age must be between 18 and 120"])
$ gleam test
   Compiled in …s
    Running basics_test.main
.....
5 passed, no failures
$ gleam test --target javascript
  Compiling gleam_stdlib
  Compiling gleeunit
  Compiling basics
   Compiled in …s
    Running basics_test.main
.....
5 passed, no failures

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 pipes_session.py

The pipeline printed Asha · Meera · Ravi, both calls of total gave 580, and validate returned the three problems of the second form in one Error, where the JavaScript version threw after the first. All five tests passed on Erlang and on JavaScript.

Fail fast or collect

use with result.try stops at the first Error, which suits steps that depend on each other, such as parsing an age only after the split worked. A form whose fields are independent checks each one and collects every message, as validate does with one case over its three results.

Checkpoint

Check: Pipes, labelled arguments and echo

  1. Question 1 of 2 Which call does the pipeline below make?

    Read the code, then choose one answer.

    "  hello  "
    |> string.trim
    |> string.append("!")
    Show the answer: Question 1 of 2

    Answer: string.append(string.trim(" hello "), "!"), which gives "hello!"

    x |> f(y) is f(x, y). That is why the pipeline in pipes.gleam reads in the order the data changes.

  2. Question 2 of 2 total is declared as total(prices: List(Int), discount percent: Int, delivery fee: Int). Which call is the same as total([100], discount: 10, delivery: 40)?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: total([100], delivery: 40, discount: 10)

    In a declaration, the first word is the label callers write and the second is the name in the body. Labels make a call readable and let its arguments come in any order after the unlabelled ones.

Whole numbers: Gleam is neither Python nor JavaScript

Integer arithmetic is where ported code goes wrong without a single error. Gleam’s / on two Ints drops the fraction, rounding towards zero, which is truncating division: Integer division that drops the fraction, rounding towards zero, so -7 divided by 2 gives -3. C, Objective-C and Visual Basic's \ divide this way, and their remainder takes the dividend's sign. More in “A one-week plan for learning any language”; Python’s // rounds down instead, which is floor division: Integer division that rounds the quotient down, towards minus infinity, so -7 divided by 2 gives -4. Tcl's / on integers and Python's // divide this way, and their remainder takes the divisor's sign. More in “A one-week plan for learning any language”. Gleam defines division by zero as zero, where Python raises an error and JavaScript gives Infinity. The same table was run three times: in Gleam on both targets, in Python 3.14.8 and in Node.js 24.21.0:

numbers.gleam: division, rounding, parsing and large values GLEAM · numbers/src/numbers.gleam
import gleam/float
import gleam/int
import gleam/io
import gleam/string

fn show(expression: String, value: String) -> Nil {
  io.println(string.pad_end(expression, 26, " ") <> value)
}

pub fn main() -> Nil {
  show("-7 / 2", int.to_string(-7 / 2))
  show("-7 % 2", int.to_string(-7 % 2))
  show("int.floor_divide(-7, 2)", string.inspect(int.floor_divide(-7, 2)))
  show("int.modulo(-7, 2)", string.inspect(int.modulo(-7, 2)))
  show("7 / 0", int.to_string(7 / 0))
  show("int.divide(7, 0)", string.inspect(int.divide(7, 0)))
  show("float.round(-2.5)", int.to_string(float.round(-2.5)))
  show("float.round(2.5)", int.to_string(float.round(2.5)))
  show("9_007_199_254_740_993", int.to_string(9_007_199_254_740_993))
  show("9_007_199_254_740_992 + 1", int.to_string(9_007_199_254_740_992 + 1))
  show("int.parse(\"4e1\")", string.inspect(int.parse("4e1")))
  show("int.parse(\"1_000\")", string.inspect(int.parse("1_000")))
  show("string.inspect(1.0)", string.inspect(1.0))
}
The table on the BEAM and on Node.js Python · numbers_session.py
from session import run

# Integer arithmetic and the size of an Int, on the BEAM and on Node.js. The JavaScript build warns about the two
# literals that a 64-bit float cannot hold exactly.
run(
    """
cd numbers
gleam run
gleam run --target javascript
"""
)

Output

$ cd numbers
$ gleam run
Downloading packages
 Downloaded 2 packages in …s
  Compiling gleam_stdlib
  Compiling gleeunit
  Compiling numbers
   Compiled in …s
    Running numbers.main
-7 / 2                    -3
-7 % 2                    -1
int.floor_divide(-7, 2)   Ok(-4)
int.modulo(-7, 2)         Ok(1)
7 / 0                     0
int.divide(7, 0)          Error(Nil)
float.round(-2.5)         -3
float.round(2.5)          3
9_007_199_254_740_993     9007199254740993
9_007_199_254_740_992 + 1 9007199254740993
int.parse("4e1")          Error(Nil)
int.parse("1_000")        Error(Nil)
string.inspect(1.0)       1.0
$ gleam run --target javascript
  Compiling gleam_stdlib
  Compiling gleeunit
  Compiling numbers
warning: Int is outside JavaScript's safe integer range
   ┌─ src/numbers.gleam:19:47
   │
19 │   show("9_007_199_254_740_993", int.to_string(9_007_199_254_740_993))
   │                                               ^^^^^^^^^^^^^^^^^^^^^ This is not a safe integer value on JavaScript

This integer value is too large to be represented accurately by
JavaScript's number type. To avoid this warning integer values must be in
the range -(2^53 - 1) - (2^53 - 1).

See JavaScript's Number.MAX_SAFE_INTEGER and Number.MIN_SAFE_INTEGER
properties for more information.

warning: Int is outside JavaScript's safe integer range
   ┌─ src/numbers.gleam:20:51
   │
20 │   show("9_007_199_254_740_992 + 1", int.to_string(9_007_199_254_740_992 + 1))
   │                                                   ^^^^^^^^^^^^^^^^^^^^^ This is not a safe integer value on JavaScript

This integer value is too large to be represented accurately by
JavaScript's number type. To avoid this warning integer values must be in
the range -(2^53 - 1) - (2^53 - 1).

See JavaScript's Number.MAX_SAFE_INTEGER and Number.MIN_SAFE_INTEGER
properties for more information.

   Compiled in …s
    Running numbers.main
-7 / 2                    -3
-7 % 2                    -1
int.floor_divide(-7, 2)   Ok(-4)
int.modulo(-7, 2)         Ok(1)
7 / 0                     0
int.divide(7, 0)          Error(Nil)
float.round(-2.5)         -3
float.round(2.5)          3
9_007_199_254_740_993     9007199254740992
9_007_199_254_740_992 + 1 9007199254740992
int.parse("4e1")          Error(Nil)
int.parse("1_000")        Error(Nil)
string.inspect(1.0)       1

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 numbers_session.py

The same arithmetic in Python and in JavaScript

compare/compare.py

# The same arithmetic and parsing in Python 3.14, for the table of the lesson.
print("-7 // 2       ", -7 // 2)
print("-7 % 2        ", -7 % 2)
print("round(-2.5)   ", round(-2.5))
print("round(2.5)    ", round(2.5))
print("2**53 + 1     ", 2**53 + 1)
try:
    print(7 // 0)
except ZeroDivisionError as error:
    print("7 // 0        ", "ZeroDivisionError:", error)
for text in [" 42", "4e1", "0x20", "1_000", ""]:
    try:
        value = int(text)
    except ValueError:
        value = "ValueError"
    print(f"int({text!r})".ljust(14), value)

Output

-7 // 2        -4
-7 % 2         1
round(-2.5)    -2
round(2.5)     2
2**53 + 1      9007199254740993
7 // 0         ZeroDivisionError: division by zero
int(' 42')     42
int('4e1')     ValueError
int('0x20')    ValueError
int('1_000')   1000
int('')        ValueError

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 compare.py

compare/compare.mjs

// The same arithmetic and parsing in JavaScript (Node.js 24), for the table of the lesson.
console.log("Math.trunc(-7 / 2)", Math.trunc(-7 / 2));
console.log("-7 % 2            ", -7 % 2);
console.log("Math.round(-2.5)  ", Math.round(-2.5));
console.log("Math.round(2.5)   ", Math.round(2.5));
console.log("2 ** 53 + 1       ", 2 ** 53 + 1);
console.log("7 / 0             ", 7 / 0);
for (const text of [" 42", "4e1", "0x20", "1_000", ""]) {
  console.log(`Number(${JSON.stringify(text)})`.padEnd(18), Number(text));
}

Output

Math.trunc(-7 / 2) -3
-7 % 2             -1
Math.round(-2.5)   -2
Math.round(2.5)    3
2 ** 53 + 1        9007199254740992
7 / 0              Infinity
Number(" 42")      42
Number("4e1")      40
Number("0x20")     32
Number("1_000")    NaN
Number("")         0

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node compare.mjs

Expression Gleam, both targets Python 3.14.8 JavaScript, Node.js 24.21.0
−7 divided by 2 -7 / 2 is −3 -7 // 2 is −4 Math.trunc(-7 / 2) is −3
−7 remainder 2 -7 % 2 is −1 -7 % 2 is 1 -7 % 2 is −1
7 divided by 0 7 / 0 is 0; int.divide(7, 0) is Error(Nil) ZeroDivisionError Infinity
−2.5 rounded float.round(-2.5) is −3 round(-2.5) is −2 Math.round(-2.5) is −2
2^53 + 1 9007199254740993 on the BEAM, 9007199254740992 on Node.js 9007199254740993 9007199254740992
"4e1" read as a whole number int.parse gives Error(Nil) int raises ValueError Number gives 40
"1_000" read as a whole number int.parse gives Error(Nil) int gives 1000 Number gives NaN

Python’s floor division is int.floor_divide(-7, 2) in Gleam, Ok(-4), and Python’s % is int.modulo, Ok(1); both return a Result because the divisor may be zero. float.round rounds a half away from zero on both targets, unlike Python’s round, which rounds to even, and JavaScript’s Math.round, which rounds a half up. The only row where Gleam’s two targets disagree is 2^53 + 1: the tour says an Int on JavaScript is a 64-bit float, so past Number.MAX_SAFE_INTEGER, 2^53 − 1, it loses whole units, and the compiler warned about both literals in the JavaScript build. Note also the last line of the session: string.inspect(1.0) printed 1.0 on the BEAM and 1 on Node.js.

IEEE 754 Floating-Point Converter Enter 9007199254740993 to see the 64-bit float a JavaScript Int becomes: 9007199254740992.

Porting Python's // as Gleam's /

(total - paid) // 2 in Python and { total - paid } / 2 in Gleam agree for positive numbers and differ by one for a negative one that does not divide evenly: −4 against −3 for −7. Use int.floor_divide when the old code floored, and add a test with a negative odd value.

Checkpoint

Check: Whole numbers compared with Python and JavaScript

  1. Question 1 of 2 Python code computes (-7) // 2. You port it to Gleam as -7 / 2. By how much does the Gleam result differ from the Python one?

    Type a number.

    Show the answer: Question 1 of 2

    Answer: 1

    Python's // floors, giving -4; Gleam's / truncates towards zero, giving -3: a difference of 1. int.floor_divide gives Python's -4, as Ok(-4).

  2. Question 2 of 2 A Gleam service counts bytes and a page shows the same count with the JavaScript build. The count passes 2^53. What do you expect?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The page can show a different number from the service, and the compiler warns only about literals it can see

    Results agree only while values stay within what a 64-bit float holds exactly. Keep such counters on the BEAM, or send them to the page as text.

Code that needs the BEAM does not build for JavaScript

Most of the standard library has an implementation for each target, so basics passed its tests on both. Some packages are written for one target only: gleam_erlang, which provides process (BEAM): A lightweight unit of execution on the BEAM, with its own memory and its own mailbox. Processes share nothing and work together by sending each other messages; in Gleam, process.spawn from gleam_erlang starts one. More in “Gleam processes, actors and FFI” and the rest of Erlang’s runtime, has externals in Erlang and none in JavaScript. erlang_only.gleam spawns a process that sends a message back:

erlang_only.gleam: a process sends one message GLEAM · erlang_only/src/erlang_only.gleam
import gleam/erlang/process
import gleam/io

pub fn main() -> Nil {
  let subject = process.new_subject()
  process.spawn(fn() { process.send(subject, "hello from another process") })
  let assert Ok(message) = process.receive(subject, within: 1000)
  io.println(message)
}
The same program on the BEAM, then for JavaScript Python · target_session.py
from session import run

# A program that uses gleam/erlang/process, from the Erlang-only package gleam_erlang, on both targets. The
# JavaScript build also prints warnings about gleam_erlang's own code, so its output goes to build.log; the session
# counts the warnings, those of them that point into gleam_erlang's source and the Unsupported target errors, and
# shows the first error.
run(
    """
cd erlang_only
gleam run
!gleam run --target javascript > build.log 2>&1
grep -c '^warning' build.log
grep -A 1 '^warning' build.log | grep -c 'build/packages/gleam_erlang/'
grep -c '^error: Unsupported target' build.log
grep -m 1 -A 9 '^error' build.log
"""
)

Output

$ cd erlang_only
$ gleam run
Downloading packages
 Downloaded 3 packages in …s
  Compiling gleam_stdlib
  Compiling gleam_erlang
  Compiling gleeunit
  Compiling erlang_only
   Compiled in …s
    Running erlang_only.main
hello from another process
$ gleam run --target javascript > build.log 2>&1
[exit status 1]
$ grep -c '^warning' build.log
6
$ grep -A 1 '^warning' build.log | grep -c 'build/packages/gleam_erlang/'
6
$ grep -c '^error: Unsupported target' build.log
4
$ grep -m 1 -A 9 '^error' build.log
error: Unsupported target
  ┌─ src/erlang_only.gleam:5:25
  │
5 │   let subject = process.new_subject()
  │                         ^^^^^^^^^^^

This value is not available as it is defined using externals, and there is
no implementation for the JavaScript target.

Hint: Did you mean to build for a different target?

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 target_session.py

On the BEAM it printed hello from another process. For JavaScript the build failed: it printed six warnings, all six about gleam_erlang’s own code, and four Unsupported target errors, one at each use of a process function. The first error, shown in full, says the value “is defined using externals” and has no implementation for the JavaScript target. A build compiles every module in src for its target, so one Erlang-only call in any of them stops the JavaScript build of the whole project. Step through which modules of this lesson build where:

Interactive · step by step Which modules build for which target Step through the two builds of this lesson's modules, first the shared ones, then the Erlang-only one.
Which modules build for which target, step 6 of 6. Showing basics modules, gleam_stdlib, erlang_only.gleam, gleam/erlang/process, Erlang build, JavaScript build. In focus: erlang_only.gleam, basics modules.importsimportsbasics modulesgleam_stdliberlang_only.gleamgleam/erlang/processErlang buildJavaScript build

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Shared modules. shapes, results, pipes and form import only gleam_stdlib, which has an implementation for both targets.
  2. Both builds pass. gleam test and gleam test --target javascript both ran the five tests of basics and passed.
  3. An Erlang-only import. erlang_only.gleam imports gleam/erlang/process from gleam_erlang, whose functions are externals written in Erlang only.
  4. On the BEAM. The Erlang build spawned a process that sent hello from another process back to main.
  5. On JavaScript. The JavaScript build stopped with four Unsupported target errors, one for each use of a process function, after six warnings from gleam_erlang's own code.
  6. The fix is the layout. Keep code that needs the BEAM in modules that only the Erlang build uses, or give a function an external for each target, as the next lesson shows.

So a package that must run on both targets keeps its target-specific code at the edges: shared modules import only what builds everywhere, such as gleam_stdlib, and code that needs processes lives in modules only the Erlang build uses. When one function must exist on both, it can name an external implementation for each target, which the next lesson writes.

Let the type checker see your input

Values from outside, such as a form, a file or a request, arrive as text. Turn them into typed values once, at the edge, with functions that return Result, as validate does, and keep let assert away from them: a crafted input then gets an error message instead of crashing the process that read it.

Checkpoint

Check: Code that builds for one target only

  1. Question 1 of 2 Why did gleam run --target javascript fail for erlang_only?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: process.new_subject and the other process functions are externals with an Erlang implementation only

    gleam_erlang gives Erlang's processes to Gleam through externals written in Erlang only, so every use of them is a compile error on the JavaScript target.

  2. Question 2 of 2 A package that builds for both targets has a pure module, rules.gleam. A teammate adds a call to process.sleep inside it. What happens?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The JavaScript build of the whole package fails with Unsupported target

    Keep shared modules free of target-only imports, and build and test both targets in CI, so such a change fails on the branch.

Practise: port a form check that throws

form_before.mjs is the JavaScript that form.gleam replaced. Run in its own file, it shows both of its bugs: the second form reported only name is required, and the third was accepted with an age of 40, read from "4e1":

form_before.mjs: throws at the first problem, trusts Number() JavaScript · compare/form_before.mjs
// A form check as JavaScript code often writes it: it throws at the first problem.
function validate(name, ageText, email) {
  if (name.trim() === "") throw new Error("name is required");
  const age = Number(ageText);
  if (!Number.isInteger(age)) throw new Error("age must be a whole number");
  if (age < 18 || age > 120) throw new Error("age must be between 18 and 120");
  const parts = email.trim().split("@");
  if (parts.length !== 2 || parts[0] === "" || parts[1] === "") {
    throw new Error("email needs one @ with text on both sides");
  }
  return { name: name.trim(), age, email: email.trim() };
}

for (const fields of [[" Asha ", "31", "asha@example.com"], ["", "4e1", "ravi.example.com"], ["Ravi", "4e1", "ravi@example.com"]]) {
  try {
    console.log(validate(...fields));
  } catch (error) {
    console.log("Error:", error.message);
  }
}

Output

{ name: 'Asha', age: 31, email: 'asha@example.com' }
Error: name is required
{ name: 'Ravi', age: 40, email: 'ravi@example.com' }

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node form_before.mjs

The exercise ports form.gleam’s rules to Python, so that its sample tests run in your browser: return every problem in field order, and read the age the way int.parse does, which neither JavaScript’s Number nor Python’s int does on its own:

Exercise · Easy · Python

Check a form the way form.gleam does

form_before.mjs throws at the first problem, and its Number(ageText) reads "4e1" as 40. Port the rules of form.gleam to Python instead: write validate(name, age, email) in form.py, returning ("ok", form) with form a dict of name, age and email, or ("error", problems) with every problem, in field order. It mirrors Gleam's Result(Form, List(String)).

  • name, trimmed, must not be empty: "name is required".
  • age, trimmed, must be a whole number written with digits only, an optional + or - first, the way int.parse reads it: otherwise "age must be a whole number". A number below 18 or above 120 gives "age must be between 18 and 120".
  • email, trimmed, must split at @ into exactly two parts, neither empty: "email needs one @ with text on both sides".

The checked form holds the trimmed name and email and the age as an int. Do not use Python's int() on its own: it accepts " 42", "1_000" and other text that int.parse refuses.

Starter code · form.py

def validate(name, age, email):
    """("ok", {"name", "age", "email"}) or ("error", [every problem, in field order])."""
    # Replace this with your code: it stops at the first problem, as form_before.mjs does, and trusts int().
    if name.strip() == "":
        return ("error", ["name is required"])
    return ("ok", {"name": name.strip(), "age": int(age), "email": email.strip()})
The sample tests · test_form.py
from form import validate


def test_a_good_form_is_trimmed():
    """the first call of form.main: Ok(Form("Asha", 31, "asha@example.com"))"""
    assert validate(" Asha ", "31", "asha@example.com") == ("ok", {"name": "Asha", "age": 31, "email": "asha@example.com"})


def test_every_problem_in_field_order():
    """the second call of form.main: three problems, not only the first"""
    assert validate("", "4e1", "ravi.example.com") == (
        "error",
        ["name is required", "age must be a whole number", "email needs one @ with text on both sides"],
    )


def test_an_age_out_of_range():
    """the third call of form.main"""
    assert validate("Meera", "12", "meera@example.com") == ("error", ["age must be between 18 and 120"])


def test_text_that_javascript_reads_as_a_number():
    """Number("0x20") is 32 in JavaScript; int.parse refuses it"""
    assert validate("Ravi", "0x20", "ravi@example.com") == ("error", ["age must be a whole number"])


def test_text_that_python_reads_as_a_number():
    """int("1_000") is 1000 in Python; int.parse refuses it"""
    assert validate("Ravi", "1_000", "ravi@example.com") == ("error", ["age must be a whole number"])


def test_two_at_signs():
    """split at @ gives three parts"""
    assert validate("Joe", "40", "joe@@example.com") == ("error", ["email needs one @ with text on both sides"])
A hint

Check each field into its own variable first, either a value or a message, then decide: no messages gives ("ok", …), otherwise ("error", messages) in the order name, age, email. For the age, a regular expression such as ^[+-]?[0-9]+$ on the trimmed text accepts exactly what int.parse accepts.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

The self-check below asks first about the weakest of your five ideas, from custom types to target-only code, and explains each answer the moment you choose it:

Practice that adapts to you

Practise this lesson

10 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 10 A function may find no matching user. What does Gleam code return where JavaScript would return undefined?

    Choose one answer.

    Show the answer: Question 1 of 10

    Answer: A Result or an Option, whose type tells the caller to handle the missing case

    The type says that the value may be missing, and the compiler makes every caller take both cases apart.

  2. Question 2 of 10 A case on Shape ends with _ -> "unknown". Next month someone adds Triangle. What changes compared with naming every variant?

    Choose one answer.

    Show the answer: Question 2 of 10

    Answer: The build passes and a triangle is called unknown, because the catch-all matches it

    Naming every variant keeps the compiler's help: each new variant becomes an error at each case that must decide what to do with it.

  3. Question 3 of 10 What does int.parse("abc") return?

    Choose one answer.

    Show the answer: Question 3 of 10

    Answer: Error(Nil)

    A Result makes the failure visible in the type: parse_age turned this Error(Nil) into Error("not a number: abc").

  4. Question 4 of 10 According to the language tour, what does use n <- result.try(step) turn the lines below it into?

    Choose one answer.

    Show the answer: Question 4 of 10

    Answer: An anonymous function fn(n) { … }, passed as the last argument of result.try(step)

    use is only syntax for a callback. With result.try, the callback runs with the value inside Ok, and an Error is returned without calling it.

  5. Question 5 of 10 What does echo print, and where?

    Choose one answer.

    Show the answer: Question 5 of 10

    Answer: The file and line, then the value, on standard error; then it returns the value unchanged

    echo is a debugging aid that fits anywhere in a pipeline. Remove it before publishing: the build tool refuses a package that still has one.

  6. Question 6 of 10 A test compares string.inspect(Person("asha", 31)) with the text "Person(\"asha\", 31)". On which targets does it pass?

    Choose one answer.

    Show the answer: Question 6 of 10

    Answer: On the BEAM only: Node.js printed Person(name: "asha", age: 31)

    Compare values with ==, as the tests of basics do, and keep inspected text for debugging. string.inspect(1.0) also differed: 1.0 on the BEAM, 1 on Node.js.

  7. Question 7 of 10 Which Gleam call gives Python's result for -7 % 2?

    Choose one answer.

    Show the answer: Question 7 of 10

    Answer: int.modulo(-7, 2), which returned Ok(1)

    int.modulo and int.floor_divide give Python's % and //, and return a Result because the divisor may be zero.

  8. Question 8 of 10 What do Gleam's float.round(-2.5), Python's round(-2.5) and JavaScript's Math.round(-2.5) give?

    Choose one answer.

    Show the answer: Question 8 of 10

    Answer: -3, -2 and -2

    Rounding rules differ between languages exactly at halves. Ported code that rounds money needs a test with a half.

  9. Question 9 of 10 Which package does gleam/erlang/process come from, and which targets does it support?

    Choose one answer.

    Show the answer: Question 9 of 10

    Answer: gleam_erlang, which supports the Erlang target only

    Packages decide which targets they support. gleam_erlang is for the BEAM, so code that uses it belongs in modules that only the Erlang build compiles.

  10. Question 10 of 10 A package must run on both targets, and one module needs processes. Which layout works?

    Choose one answer.

    Show the answer: Question 10 of 10

    Answer: Keep process code in its own package or app that only the Erlang build uses, and keep the shared modules free of it

    Design for both targets from the start: a pure core that builds everywhere, and target-specific code at the edges, or a function with an external for each target.

Interview questions

Warm-up (fresher to mid-level): How does Gleam handle an operation that can fail, without exceptions?

It returns a Result: Ok(value) when it worked and Error(reason) when it did not, and the type, such as Result(Int, String), tells every caller to handle both. int.parse("abc") returns Error(Nil) instead of throwing. The caller takes it apart with case, or chains steps with use x <- result.try(step), which carries on with the value inside Ok and returns the first Error at once; in the lesson, parse_person("ravi:abc") returned Error("not a number: abc") that way. For a value that cannot be wrong, let assert Ok(x) = … takes it out and crashes with a message if it is wrong after all; that is for programmer errors, not for user input. Gleam’s panic and the BEAM’s crashes still exist, but they are for bugs, not for control flow.

Two longer questions, one about a port from Python and one about a shared module:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to senior

    A Python service splits a bill two ways with share = (total - discount) // 2, and the port to Gleam reads { total - discount } / 2. Every test passes, yet a refund of -7 now gives -3 each where Python gave -4. Explain the difference, fix the port, and say how you would have caught it.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    Your Gleam package runs its pricing rules on the BEAM and in the browser. After a teammate adds a small retry with process.sleep to the rules module, CI's JavaScript build fails with Unsupported target, while the Erlang tests pass. What is going on, and how do you fix the design?

    Model answer, Pro. First step and follow-ups

Flashcards · 8 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. A fourth variant, Triangle, is added to a Gleam type. What happens to the case expressions over it?

    Every one that does not handle Triangle stops the build with Inexhaustive patterns, listing the missing pattern. A catch-all _ branch would hide it instead, so name every variant.

  2. How does a Gleam function say that it can fail?

    It returns the built-in Result: Ok(value) or Error(reason), as in Result(Int, String), and every caller must handle both. Gleam uses no exceptions for expected failures.

  3. What does use age <- result.try(parse_age(text)) do with the lines below it?

    They become a function that result.try calls with the value inside Ok. An Error is returned at once, so a chain of failing steps stays flat.

  4. When is let assert the right tool in Gleam?

    For a value the code has already checked and cannot work without: it crashes with your message if the pattern does not match. User input should get an Error instead.

  5. What does |> do, and what do labelled arguments add to a Gleam call?

    |> passes the value on its left as the first argument of the call on its right, so data reads left to right. Labels such as discount: and delivery: let a call give its arguments in any order.

  6. What are -7 / 2, -7 % 2 and 7 / 0 in Gleam?

    −3, −1 and 0: / truncates, and division by zero is defined as zero. int.floor_divide(-7, 2) gives Ok(-4) and int.modulo(-7, 2) gives Ok(1), Python's results, as Results.

  7. Where do Gleam's two targets give different whole numbers?

    Beyond 2^53: on the BEAM an Int has no size limit, so 2^53 + 1 is 9007199254740993, while on JavaScript an Int is a 64-bit float and gives 9007199254740992.

  8. Why does one call into gleam_erlang stop the JavaScript build of a whole project?

    gleam_erlang's functions are externals implemented in Erlang only, so each use is an Unsupported target error, and a build compiles every module in src. Shared modules import only what builds for both.

Key takeaways

  • A custom type lists every variant a value can be, and case takes it apart; adding a variant turns every case that must handle it into a compile error, as Triangle did twice.
  • Expected failures are Result values; use … <- result.try(…) stops at the first Error, a case over several results collects them all, and let assert crashes on purpose for values that cannot be wrong.
  • |> passes the value on its left as the first argument, labels make calls readable in any order, and echo prints a value with its file and line, in a format that can differ by target.
  • Gleam’s / truncates and 7 / 0 is 0, int.floor_divide and int.modulo give Python’s results, float.round rounds halves away from zero, and integers past 2^53 differ between the BEAM and JavaScript.
  • A call into an Erlang-only package such as gleam_erlang stops the JavaScript build with Unsupported target, so shared modules import only what builds for both targets.

Terms in this lesson

custom type
A Gleam type defined with pub type and one or more variants, each with its own fields, such as Shape with Circle, Rectangle and Square. A value of the type is always exactly one of its variants.
exhaustiveness check
The compiler's check that the patterns of a case expression cover every possible value. In Gleam a case that misses a variant does not compile: the error, Inexhaustive patterns, lists the missing patterns.
labelled argument
A function argument with a name callers write, such as discount: 10. A Gleam declaration gives each a label and an internal name, discount percent: Int, and labelled arguments may be passed in any order.
let assert
A Gleam assignment whose pattern may not match, such as let assert Ok(age) = parse_age(text). When it does not match, the program crashes on purpose with an optional message, so it is kept for values that cannot be wrong.
Result
Gleam's built-in type for a computation that can fail: Ok(value) when it worked, Error(reason) when it did not. Result(Int, String) holds an Int or a String reason, and the caller must handle both.
use expression
Gleam syntax, use x <- f(a), that turns all the code below it into an anonymous function fn(x) { … } passed as the last argument of f. With result.try it carries on with an Ok value or returns the Error at once.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.