Processes (1/3)

Feb 25, 2026   Kero van Gelder
In this series we are going to look at managing concurrency in Gleam in the following ways:
  1. processes, this post
  2. actors and subjects (not yet published)
  3. supervised actors (not yet published)
This series supersedes an earlier series of 3 posts that I made in 2024 on this same blog.

Processes

A process on the beam is an independently running piece of software. It: Thus, a process runs independently from any other process. It cannot disturb other processes except by consuming CPU for 1/nth of the total and it cannot be disturbed by other processes.

A gleam actor is a process, processing incoming messages. A supervisor is also a process, whose only job it is to keep an eye on other processes. Registries are processes that can look things up for you, possibly processes.

Be good

import gleam/erlang/process

pub fn main() {
  process.spawn(fn() { echo "Hello, world!" })
}
$ gleam run -m spawn_002
   Compiled in 0.02s
    Running spawn_002.main
src/spawn_002.gleam:4
"Hello, world!"
Too easy. A new process was spawned, running the function we passed it. The function gave us a friendly debug message and fininshed, after which the process terminated normally. Not all that exciting.

Be Bad

import gleam/erlang/process

pub fn main() {
  process.spawn(fn() { panic as "Hello, world!" })
  process.sleep(123)
}
$ gleam run -m spawn_panic_004
   Compiled in 0.05s
    Running spawn_panic_004.main
=CRASH REPORT==== 25-Feb-2026::17:57:27.407061 ===
  crasher:
    initial call: spawn_panic_004:'-main/0-anonymous-0-'/0
    pid: <0.84.0>
    registered_name: []
    exception error: #{function => <<"main">>,line => 4,
                       message => <<"Hello, world!">>,
                       module => <<"spawn_panic_004">>,
                       file => <<"src/spawn_panic_004.gleam">>,
                       gleam_error => panic}
      in function  spawn_panic_004:'-main/0-anonymous-0-'/0 (src/spawn_panic_004.gleam:6)
    ancestors: [<0.83.0>]
    message_queue_len: 0
    messages: []
    links: [<0.83.0>]
    dictionary: []
    trap_exit: false
    status: running
    heap_size: 233
    stack_size: 29
    reductions: 19
  neighbours:
    neighbour:
      pid: <0.83.0>
      registered_name: []
      initial_call: {erlang,apply,2}
      current_function: {erlang,prepare_loading_1,2}
      ancestors: []
      message_queue_len: 0
      links: [<0.10.0>,<0.84.0>]
      trap_exit: false
      status: running
      heap_size: 233
      stack_size: 13
      reductions: 414
      current_stacktrace: [{erlang,prepare_loading_1,2,[]},
                  {code,ensure_loaded,1,[{file,"code.erl"},{line,582}]},
                  {error_handler,undefined_function,3,
                      [{file,"error_handler.erl"},{line,86}]},
                  {processes@@main,run_module,1,
                      [{file,
                           "/home/kero/CodeChange/new-web-content/gleam-blog/20260225-2-processes/build/dev/erlang/processes/_gleam_artefacts/processes@@main.erl"},
                       {line,27}]}]
runtime error: panic

Hello, world!

stacktrace:
  spawn_panic_004.-main/0-anonymous-0- src/spawn_panic_004.gleam:4
  proc_lib.init_p proc_lib.erl:317

Whoops! Our process terminated abnormally, and worse, it took out our fancy application.

In Gleam, processes start linked, meaning that when one a process terminated abnormally, all linked processes are sent a special exit message. Without any precaution, that means linked processes also terminate. The list of neightbours contains one neighbour, our main function (pid 83 is linked to pid 84, so it is our main function, but I have trouble understanding what that error handler is doing).

! Such a link goes in both directions. If we were to panic in our main function, the spawned process would go down with it.

! Do note the process.sleep. We need our main process to be alive when our spawned function does. Without sleeping, it might have terminated already, or it might not - that is what concurrency is.

! We did not need to sleep in our previous example. I/O is done by a special process started by the BEAM. Even though our main function had terminated, current gleam (1.14) waits for one second after that. That is enough time for the I/O process to do its printing.

Spawn Unlinked

import gleam/erlang/process

pub fn main() {
  process.spawn_unlinked(fn() { panic as "Hello, world!" })
  process.sleep(123)
}
  Compiling processes
   Compiled in 0.60s
    Running spawn_unlinked_007.main
=CRASH REPORT==== 25-Feb-2026::17:56:51.541772 ===
  crasher:
    initial call: spawn_unlinked_007:'-main/0-anonymous-0-'/0
    pid: <0.84.0>
    registered_name: []
    exception error: #{function => <<"main">>,line => 4,
                       message => <<"Hello, world!">>,
                       module => <<"spawn_unlinked_007">>,
                       file => <<"src/spawn_unlinked_007.gleam">>,
                       gleam_error => panic}
      in function  spawn_unlinked_007:'-main/0-anonymous-0-'/0 (src/spawn_unlinked_007.gleam:6)
    ancestors: [<0.83.0>]
    message_queue_len: 0
    messages: []
    links: []
    dictionary: []
    trap_exit: false
    status: running
    heap_size: 233
    stack_size: 29
    reductions: 19
  neighbours:

OK, that is better. Some logger still dumped the crash report on stdout, and as you can see the list of neightbours is now empty. Our main function was allowed to finish in peace.

Monitoring a process

Our process died. It was doing some very important things. We sometimes want to know that it terminated, so we can clean up after it or some such thing.

import gleam/erlang/process

pub fn main() {
  let #(_pid, mon) = spawn_monitored(fn() { panic as "Hello, world!" })
  process.new_selector()
  |> process.select_specific_monitor(mon, Wrap)
  |> process.selector_receive(123)
  |> echo
}

pub type Msg {
  Wrap(process.Down)
}

@external(erlang, "erlang", "spawn_monitor")
fn spawn_monitored(f: fn() -> Nil) -> #(process.Pid, process.Monitor)
$ gleam run -m spawn_monitored_010
   Compiled in 0.02s
    Running spawn_monitored_010.main
src/spawn_monitored_010.gleam:8
Ok(Wrap(ProcessDown(//erl(#Ref<0.3063925226.1700003845.102100>), //erl(<0.84.0>), Abnormal(#(dict.from_list([#(Function, "main"), #(Line, 4), #(Message, "Hello, world!"), #(Module, "spawn_monitored_010"), #(File, "src/spawn_monitored_010.gleam"), #(GleamError, Panic)]), [SpawnMonitored10(atom.create("-main/0-anonymous-0-"), 0, [File(charlist.from_string("src/spawn_monitored_010.gleam")), Line(7)])])))))
=ERROR REPORT==== 25-Feb-2026::18:34:52.022236 ===
Error in process <0.84.0> with exit value:
{#{function => <<"main">>,line => 4,message => <<"Hello, world!">>,
   module => <<"spawn_monitored_010">>,
   file => <<"src/spawn_monitored_010.gleam">>,gleam_error => panic},
 [{spawn_monitored_010,'-main/0-anonymous-0-',0,
                       [{file,"src/spawn_monitored_010.gleam"},{line,7}]}]}

Quite the message we receive.

! We have an error report now, not a crash report.

! This erlang function is not in the gleam library (yet?)

Trapping exits

You can even trap exits of linked processes, as I looked at in my original blog post. It seems no better than monitoring in any way that I can think of, therefor I am not giving you a code example again.