Gleam is a type-safe and scalable language for the Erlang virtual machine and JavaScript runtimes. Today Gleam v1.6.0 has been published, featuring so many excellent improvements that I struggled to title this post. Let’s take a look at them now, but first, the Gleam developer survey!

This is your chance to shape our 2025 plans, so fill it in and share with your friends! Any level of Gleam experience is OK (or even none yet at all), so please fill it in so we can use your feedback to decide what to focus on in the coming year.

Gleam Developer Survey

Right. Now on to the release changes.

Context aware errors

A big part of what makes Gleam productive is the way its powerful static analysis can immediately provide you with feedback as you type, enabling you to confidently make changes within large or unfamiliar codebases. Much of this feedback will come in the form of error messages, so it is vital that they are as clear and as understandable as possible.

With this release Gleam’s errors are now context aware. Using data from the compiler’s code analysis system type errors now use the names and syntax that the programmer would use within that specific area of the code.

For example, here is some code with a type error.

import gleam/order

pub fn run(value: order.Order) -> Int {
  100 + value
}
error: Type mismatch
  ┌─ /src/problem.gleam:4:9
  │
4 │   100 + value
  │         ^^^^^

The + operator expects arguments of this type:

    Int

But this argument has this type:

    order.Order

Notice how the Order type is qualified with the module name, the same as the programmer would write in this module. If the module is aliased when imported then that alias will also be used in the error.

-import gleam/order
+import gleam/order as some_imported_module

-pub fn run(value: order.Order) -> Int {
+pub fn run(value: some_imported_module.Order) -> Int {
  100 + value
}
error: Type mismatch
  ┌─ /src/problem.gleam:4:9
  │
4 │   100 + value
  │         ^^^^^

The + operator expects arguments of this type:

    Int

But this argument has this type:

-   order.Order
+   some_imported_module.Order

Or the type could be imported in an unqualified fashion, in which case the error would not have the redundant qualifier.

-import gleam/order
+import gleam/order.{type Order}

-pub fn run(value: order.Order) -> Int {
+pub fn run(value: Order) -> Int {
  100 + value
}
error: Type mismatch
  ┌─ /src/problem.gleam:4:9
  │
4 │   100 + value
  │         ^^^^^

The + operator expects arguments of this type:

    Int

But this argument has this type:

-   order.Order
+   Order

You could even define your own types that hide prelude types like Int, Float, and String, at which point the prelude types would be displayed with a qualifier.

Perhaps don’t write code like this though, your coworkers probably won’t be happy with you if you do.

pub type Int
pub type String

pub fn run() {
  [100, "123"]
}
error: Type mismatch
  ┌─ /src/thingy.gleam:6:9
  │
6 │   [100, "123"]
  │         ^^^^^

All elements of a list must be the same type, but this one doesn't
match the one before it.

Expected type:

    gleam.Int

Found type:

    gleam.String

These context-aware errors should reduce the mental overhead of understanding them, making Gleam programming easier and more productive. Thank you Surya Rose for this!

Context aware editing hovering

Not being satisfied with improving only the error messages, Surya has also made the language server hovering context aware. This means that if you hover over Gleam code in your editor the type information will be shown using the appropriate names and syntax for that module.

import gleam/option

const value = option.Some(1)
//    ^ hovering here shows `option.Option(Int)`
import gleam/option.{type Option as Maybe}

const value = option.Some(1)
//    ^ hovering here shows `Maybe(Int)`

Thank you again Surya!

Add annotations code action

In Gleam all type annotations are optional, full analysis is always performed. Adding annotations does not make your code more safe or well typed, but we still think it’s a good idea to add them to make your code easier to read.

If your colleague has sadly forgotten this and not written any annotations for their functions, you can use the language server’s new code action within your editor to add missing annotations. Place your cursor within this function that is missing annotations:

pub fn add_int_to_float(a, b) {
  a +. int.to_float(b)
}

And after triggering the code action this code becomes:

pub fn add_int_to_float(a: Float, b: Int) -> Float {
  a +. int.to_float(b)
}

Thanks to, you guessed it, Surya Rose for this new feature!

Erlang compilation daemon

When targeting the Erlang virtual machine the build tool makes use of the Erlang compiler to generate BEAM bytecode, taking advantage of all of its optimisations. The Erlang compiler is written in Erlang, so this would involve booting the virtual machine and the compiler once per dependency. On a slow machine this could take as much as half a second each time, which would add up and slow down from-scratch build times.

The build tool now boots one instance of the virtual machine and sends code to it for compilation when needed, completely removing this cost. This change will be most impactful for clean builds such when changing Gleam version or in your CI pipeline, or in monorepos of many packages.

Thank you yoshi for this!

Variant inference

The compiler now infers and keeps track of the variant of custom types within expressions that construct or pattern match on them. Using this information it can now be more precise with exhaustiveness checking, field access, and record updates.

That’s not the clearest explanation, so here’s some examples. Imagine a custom type named Pet which has two variants, Dog and Turtle.

pub type Pet {
  Dog(name: String, cuteness: Int)
  Turtle(name: String, speed: Int, times_renamed: Int)
}

Any place where a value of type Pet is used, the code would have to take into account both variants, even if it seems obvious to us as humans that the value is definitely a specific variant at this point in the code. The compiler would still require that you take the other variants into account.

With variant inference you now no longer need to include patterns for the other variants.

pub fn main() {
  // We know `charlie` is a `Dog`...
  let charlie = Dog("Charles", 1000)

  // ...so you do not need to match on the `Turtle` variant
  case charlie {
    Dog(..) -> todo
  }
}

It also works for the record update syntax. This code would previously fail to compile due to the compiler not being able to tell that pet is the right variant.

pub fn rename(pet: Pet, to name: String) -> Pet {
  case pet {
    Dog(..) -> Dog(..pet, name:)
    Turtle(..) -> Turtle(..pet, name:, times_renamed: pet.times_renamed + 1)
  }
}

It also works for the field accessor syntax, enabling their use with fields that do not exist on all of the variants.

pub fn speed(pet: Pet) -> Int {
  case pet {
    Dog(..) -> 500

    // Here the speed field can be safely accessed even though
    // it only exists on the `Turtle` variant.
    Turtle(..) -> pet.speed
  }
}

Thank you Surya Rose, the superstar of this release!

Precise dependency updates

The gleam update command can be used to update your dependencies to the latest versions compatible with the requirements specified in gleam.toml.

You can now also use this command to update a specific set of dependencies, rather than all of them. If I wanted to update lustre and gleam_json I could run this command:

gleam update lustre gleam_json

Thank you Jason Sipula for this feature!

When a package is published Gleam will also generate and upload HTML documentation for users to read. This documentation includes links to the source code in its repository, assuming it is hosted using a known service such as Forgejo or GitHub.

Unfortunately these links would not be accurate if you were using a monorepo or if the package was not located at the root of the repository for some other reason.

To resolve this the repository config in gleam.toml can now optionally include a path so Gleam knows how to build the correct URLs.

[repository]
type = "github"
user = "pink-inc"
repo = "monorepo"
path = "packages/fancy_package"

Thank you Richard Viney!

Result handling hints

Errors are represented as values in Gleam, and exceptions are rare and not used for flow control. This means that Gleam programmers will very often be using the Result type in their programs, which can be initially confusing to programmers more familiar with exception based error handling.

One common stumbling block is how to use a value that is wrapped in a Result. To help with this the compiler can now suggest to pattern match on these result values when appropriate.

error: Type mismatch
  ┌─ /src/one/two.gleam:6:9
  │
6 │   int.add(1, not_a_number)
  │              ^^^^^^^^^^^^

Expected type:

    Int

Found type:

    Result(Int, a)

Hint: If you want to get a `Int` out of a `Result(Int, a)` you can pattern
match on it:

    case result {
      Ok(value) -> todo
      Error(reason) -> todo
    }

Thank you Giacomo Cavalieri!

Optional dependencies are now optional

Gleam uses Hex, the package manager for the BEAM ecosystem. It has a concept of optional packages, allowing a package to specify a version constraint on another package without causing it to be added to the dependency graph. This constraint is only used if a third package also depends on that package. This isn’t very useful in Gleam, but Elixir commonly makes use of it via its compile time metaprogramming system.

Until now the Gleam build tool would treat these optional dependencies as regular dependencies, always downloading them. With this release it now handles them correctly and will ignore packages where all constraints are marked as optional. This will reduce compile times for some Gleam projects that depend on Elixir packages.

Thank you Gustavo Inacio!

Optimised bit arrays

Gleam inherits the bit-syntax from Erlang, which is a literal syntax for constructing and parsing binary data. It can be a very nice alternative to bitwise operators commonly used in other language, and makes Gleam a pleasant language for working with binary data.

Performance wizard Richard Viney has been hard at work optimising the implementation on JavaScript, and has made their creation between 2 and 40 times faster, depending on the input data. Thank you Richard! Very impressive work!

Unsafe number warnings

When compiling to JavaScript Gleam uses JavaScript’s number types, to ensure good interop with JavaScript code and the JavaScript platform as a whole. This means that it inherits some shortcomings that do not exist on the BEAM, namely a maximum and minimum safe number size.

When targeting JavaScript the compiler now emits a warning for integer literals and constants that lie outside JavaScript’s safe integer range, letting the programmer know that their code may have unexpected behaviour due to lost precision.

warning: Int is outside the safe range on JavaScript
  ┌─ /Users/richard/Desktop/int_test/src/int_test.gleam:1:15
  │
1 │ pub const i = 9_007_199_254_740_992
  │               ^^^^^^^^^^^^^^^^^^^^^ This is not a safe integer 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.

Thank you again Richard Viney!

(un)qualification code actions

When using types and values from other Gleam modules the programmer has the choice as to whether to use them in a qualified or an unqualified fashion.

import gleam/option.{type Option}

pub fn item() -> Option(Int) { // <- unqualified
  option.Some(9) // <- qualified
}

Most commonly Gleam programmers will choose to qualify all functions, and to unqualify types that share the same name as their containing module.

Changing from one to the other is trivial, but also boring and time-consuming for types and values that are used many times in a module. The language server now has two code actions to automate this annoyance away!

Within your editor place your cursor on an instance of the type or value you wish to change, select the “qualify” or “unqualify” code action, and the language server will instantly update all the instances of that type or value in the module along with the required import statement.

Thank you Jiangda Wang for this code action!

JavaScript project creation

Gleam can compile to JavaScript as well as to Erlang. If you know your project is going to target JavaScript primarily you can now use gleam new myapp --template javascript to create a new project that is already configured for JavaScript, saving you from adding the target = "javascript" to your gleam.toml.

Thank you Mohammed Khouni for this!

And the rest

And thank you to the bug fixers and error message improvers Antonio Iaccarino, Giacomo Cavalieri, Jiangda Wang, Markus Pettersson, PgBiel, Richard Viney, Surya Rose, yoshi, and Zak Farmer!

For full details of the many fixes and improvements they’ve implemented see the changelog.

A call for support

Gleam is not owned by a corporation; instead it is entirely supported by sponsors, most of which contribute between $5 and $20 USD per month. I currently earn less than the median salary tech lead salary for London UK, the city in which I live, and Gleam is my sole source of income.

If you appreciate Gleam, please support the project on GitHub Sponsors with any amount you comfortably can. I am endlessly grateful for your support, thank you so much.

Thanks for reading, I hope you have fun with Gleam! 💜

Try Gleam