Skip to content
  • snails.dev

Note on process exits in Elixir

2023-08-14

I learned Elixir, the syntax, a while ago, now I’m in the process of learning Elixir, the semantics.

I was initially confused by the functionality around process exits. This note is my attempt of wrapping my head around the semantics and implications of process deaths, exit signals, exit trapping, and process monitoring.

Hopefully there are no glaring errors here, but I’m still learning, so all bets are off.

Exit reason and exit signals

First off, processes always exit with a reason, a value that influences how the exit plays out.

A process can exit because of (I think?) three causes. The cause of death doesn’t directly influence the exit scenario. It does influence it indirectly, through the exit reason. The causes are the following:

  • returning the result of the final expression, reason :normal;
  • raising an error, reason being the error;
  • receiving an exit signal from another process, reason contained in the signal.

Sending an exit signal can be performed by the following invocation.

Process.exit(pid, reason)

What happens when a process receives an exit signal can be represented by the following bullet list. The list is evaluated until the first condition is true.

  • If the reason in an exit signal is :kill, the process unconditionally dies.
  • Otherwise, if trapping exits, the signal is converted to a message, sent to the process, and the process doesn’t die.
  • Otherwise, if reason is not :normal, the process dies.
  • Otherwise, if the source and target of the signal are the same, the process dies.
  • Otherwise, the process doesn’t die.

Links

When a process exits, regardless of the cause, the processes linked to that process also receive an exit signal. If the reason for exit was :kill, the reason sent in the signal to the linked processes is :killed. This is to avoid recursively and unconditionally killing everything linked to the process. If the reason was different from :kill, the original reason is used.

Trapping exits

Trapping exits means that an exit signal—with reason other than :kill—is delivered to a process inbox as a normal message. The shape of the message is as follows; from being a pid:

{:EXIT, from, reason}

To enable trapping exits for given process, the process needs to call

Process.flag(:trap_exit, true)

By having trapping exits enabled for some process pid, these things change:

  • signals with :normal reason still don’t kill pid, but are not entirely ignored either;
  • signals with reason other than :normal or :kill no longer kill pid and can be reacted to;
  • death of a process linked to pid no longer spells doom for pid.

exit snippet

The above observations can be summarized by the following snippet. This is not the actual definition of exit, it’s just a simple rendering of the rules stated above. In the snippet, die is a hypothetical function cleaning up after the process.

def exit(target, reason) do
  target_info = Process.info(target)
  trapping? = Keyword.get(target_info, :trap_exit)
  linked = Keyword.get(target_info, :links)

  cond do
    reason == :kill ->
      for p <- linked, do: exit(p, :killed)
      die(target)
    trapping? ->
      send(target, {:EXIT, self(), reason})
    reason != :normal ->
      for p <- linked, do: exit(p, reason)
      die(target)
    self() == target ->
      for p <- linked, do: exit(p, :normal)
      die(target)
    true ->
      nil
  end

Hopefully, I’ll learn enough Erlang to be dangerous soon enough, and see how that’s really implemented.

Monitoring

One process—say, foo—can also monitor another process—bar. This is done by running this line in foo:

ref = Process.monitor(bar)

When bar exits, foo receives the message of the following shape:

{:DOWN, ^ref, :process, ^bar, reason}

Monitoring is unidirectional: when foo exits, bar is not notified.

Q&A

Now, a little Q&A with myself to summarize all this information.

Q: When to link two processes without trapping exits?

A: When you want an exit with non-:normal reason in one process to make the other process exit as well.

Q: When to make process foo monitor another process bar?

A: Monitoring is unidirectional, so when you want foo to be notified that bar exited, but not the other way around. Additionally, the monitoring process handles exits in the default way.

Q: Do supervisors use linking or monitoring then?

A: Linking. A dying supervisor also brings down all of its children.

Q: So when is monitoring useful?

A: I think when the monitored process can carry on even when the monitoring process dies.

Q: When to link processes with trapping exits?

A: When you want two processes to react in a custom way to each other’s exits.

Q: Do I ever want to trap exits without linking to anything?

A: I don’t know if it’s actually done in the wild, but this allows other processes to call Process.exit on foo, and have foo implement some custom logic for handling that.

Q: Why not send normal messages then?

A: You could do that, but Process.exit is there, and maybe it fits that particular use case. I guess.

Q: Do I ever want two processes to monitor each other?

A: Probably not. This is very similar to linking two processes with trapping exits; there is a subtle difference though. The processes monitoring each other handle their own exit signals in the default way.

This blog is powered by sed. And snails 🐌

Gzipped with ❤️—how else—down to 4.0K