Gleam's Promises
Gleam promises a beautiful syntax, the power of a type system, and functional programming on top of the highly concurrent BEAM VM. A very intriguing concept! But why would it have Promises?
Async Await and Promises in JavaScript
I have been playing around with NodeJS and TypeScript, and I recently built a matchmaking bot with Discord integration. I loved the type safety and the async/await paradigm, which is actually just syntactic sugar for Promises, a wonderfully simplistic API for asynchronous callbacks. Pair this with NodeJS’s single-threaded nature, and you don’t have to worry much about concurrency or multi-threading while still handling many tasks in parallel. Neat!
However, what if multi-threading and concurrency are precisely the features that you need? What if the single (active) thread that the V8 JS engine offers is not enough?
Meet the Actor Model
If you are like me and interested in distributed systems and concurrency, you have probably come across Erlang, which embraces the idea of lightweight processes and message passing: while each process runs deterministically, making it easy to debug and reason about, it can receive from and send messages to other processes via message passing. Because messages are the only way for processes to communicate, they remain nicely isolated, eliminating the need for shared variables. Processes are quick to create and destroy as well. Erlang-based applications are built on processes and concurrency from the ground up. For example, if you require a global, shared variable, you would introduce a process that implements a message-based protocol to get and set the variable and have all other processes query the variable via message passing. This design principle is known as the Actor model.
BEAM (Erlang's Virtual Machine)
While this abstraction feels a bit like overkill for a simple application, it allows Erlang to scale to thousands of processes, even across many physical nodes (using TCP to serialize messages). No wonder Erlang's virtual machine, BEAM, is now used as a base for other programming languages. Elixir, for example, has been around for some time now. And, when I played around with Elixir and Phoenix (its web framework) seven years ago, getting responses from the web server in under a millisecond was truly impressive.
Stemming from my sometimes impractical desire for type safety, I find another BEAM-based language quite charming: Gleam. Not only does Gleam offer the cutest mascot, but it also comes with a beautiful syntax (subjective, of course), static types, first-class functions, and, of course, it runs on BEAM.
Gleam's Promises in the JavaScript Runtime
But when I first got through the Gleam language tour, I was left a little confused, as there was no hint of concurrency or processes at all (there’s even an active discussion about this on GitHub). One of the main reasons is that the tour runs in your browser, of course, using JavaScript. While it is nice to be able to showcase Gleam this way, as of now, it cannot make use of BEAM and Gleam's OTP features in the browser. I would have loved seeing such examples in the tour already, because I consider the underlying concurrency Gleam's major selling point.
While Gleam doesn’t support OTP features like the actor model in JavaScript, it does come with a JavaScript module that also holds a well-designed API for Promises: https://hexdocs.pm/gleam_javascript/index.html
Even though Gleam doesn’t support async/await (discussion here), its functional approach lets you write clean code with use
already. For example, to await some some_async_function
returning data
:
import gleam/io
import gleam/javascript/promise
pub fn main() {
use data <- promise.await(some_async_function())
io.println(data)
}
Promises in Gleam's OTP Actor World
While Promises are a great abstraction for asynchronous callbacks in JavaScript, they translate poorly to the world of message passing with isolated actors. A promise represents shared mutable state: anyone holding it can observe or attach callbacks. Actors, on the other hand, are ideally isolated. To “resolve” a promise across actors, you would need a dedicated process to own it and explicitly notify every interested actor, effectively recreating a broadcast mechanism that promises hide implicitly. Hence, in Gleam's OTP actor world, it is cleaner to use explicit messages with a dedicated actor rather than relying on shared promise "objects". For small tasks, one can easily create a new, temporary process and send back a message to the initiating process using spawn.
Earlier versions of Gleam’s OTP included a Task
process abstraction, resembling async/await semantics within a single actor. This was later removed—maybe to keep concurrency primitives explicit and actor-based?
let t = task.async(fn() { do_some_work() })
res = do_some_other_work()
res + task.await(t, 100)
Summary
Gleam elegantly bridges two worlds: BEAM’s actor model and JavaScript’s async ecosystem. Promises make sense in JavaScript, but not in the world of actors.
Understanding how Gleam's concurrency story adapts to each runtime cleared up my initial confusion. And, if you haven't already, you should definitely check out Gleam yourself: https://gleam.run
Also, kindly let me know if there is any mistake in this post. After all, I just started learning Gleam. Cheers!
PS: You might also want to check out the discussions on Gleam's GitHub around JavaScript vs. BEAM: