Trapping Exits (1/3)

Mar 22, 2024   Kero van Gelder

My Gleam Program Crashes!

It is possible that a Gleam actor crashes, e.g. as in the example below. All linked processes will either receive a message, or crash themselves. Gleam actors are linked, and the creating process will also crash. If all you use is actors, your entire Gleam program will crash. This is not the promised BEAM behaviour!
import gleam/erlang/process
import gleam/otp/actor

pub fn main() {
  let assert Ok(a) = actor.start(Nil, loop)
  actor.send(a, False)

  process.sleep_forever()
}

fn loop(msg: Bool, _state: Nil) {
  let assert True = msg
  actor.continue(Nil)
}

This will crash, and generate an erlang crash dump.

We will look at three approaches to work with this, namely:

Trapping Exits

My use-case is running games. When a game finishes, I need to know that, so I can remove it from the list of games. When a game crashes, I have bigger problems, but at least want the other games to continue.

To switch behaviour from crashing, to receiving messages, we need to 'trap exits'. When we call process.trap_exits(True) before starting our actor, the program will not crash, and hence sleep forever. It will also log a stack trace for our actor, which is erlang behaviour.

So our program lives, but our actor is no more. Still not ideal. We need to learn that our actor deceased, so we can act on it.

In addition, we need to call process.selecting_trapped_exits() and wait for those messages e.g. with process.select_forever(). In my use-case, I already have a daemon that is waiting for messages, and thus has a selector. For our example, we need to create a selector, and a message type for it.

pub type MainMsg {
  ActorExited(ExitMessage)
}
  let s: Selector(MainMsg) = process.new_selector()
  let s2 = process.selecting_trapped_exits(s, ActorExited)
Inside our main function we create the selector. We specified the type for clarity. We added a variant specifically for the exit messages, and pass it to selecting_trapped_exits(). If you already have a selector and a type, you still need to add a specific variant to wrap the ExitMessage – a pattern you will see whenever you need to receive messages from multiple sources and of different types.

Waiting for the exit messages can be done as follows:

  case process.select_forever(s2) {
    ActorExited(ExitMessage(pid, reason)) -> Nil
  }
Where we get the pid and the reason. First, the reason: normal, killed, or abnormal. The latter comes with a String that represents a complex Erlang value, that looks suspiciously like a stack trace; as far as I am concerned, not usable to extract information within the program, and no need to log it since the BEAM already logged an error report.

Second, the pid. If you have mulltipe actors and need to know which one terminated, there is actor.to_erlang_start_result().

All combined gives us:
import gleam/erlang/process.{type ExitMessage, ExitMessage, type Selector}
import gleam/otp/actor

pub type MainMsg {
  ActorExited(ExitMessage)
}

pub fn main() {
  process.trap_exits(True)
  let s: Selector(MainMsg) = process.new_selector()
  let s2 = process.selecting_trapped_exits(s, ActorExited)

  let assert Ok(a) = actor.start(Nil, loop)
  let assert Ok(_pid) = actor.to_erlang_start_result(Ok(a))

  actor.send(a, False)
  case process.select_forever(s2) {
    ActorExited(ExitMessage(_pid, _reason)) -> Nil
  }
}

fn loop(msg: Bool, _state: Nil) {
  let assert True = msg
  actor.continue(Nil)
}