Li

Links in Elixir

1 exercise

About Links

Elixir processes are isolated and don't share anything by default. When an unlinked child process crashes, its parent process is not affected.

parent_pid =
  spawn(fn ->
    spawn(fn -> raise "oops" end)

    receive do
      message -> IO.inspect(message, label: "received message")
    end
  end)

# => 20:03:08.405 [error] Process #PID<0.153.0> raised an exception
#    ** (RuntimeError) oops
#    (stdlib 3.13.2) erl_eval.erl:678: :erl_eval.do_apply/6

Process.alive?(parent_pid)
# => true

This behavior can be changed by linking processes to one another. If two processes are linked, a failure in one process will be propagated to the other process. Links are bidirectional.

parent_pid =
  spawn(fn ->
    spawn_link(fn -> raise "oops" end)

    receive do
      message ->
        IO.inspect(message, label: "received message")
    end
  end)

# => 20:05:34.125 [error] Process #PID<0.171.0> raised an exception
#    ** (RuntimeError) oops
#    (stdlib 3.13.2) erl_eval.erl:678: :erl_eval.do_apply/6

Process.alive?(parent_pid)
# => false

Processes can be spawned already linked to the calling process using spawn_link/1 which is an atomic operation, or they can be linked later with Process.link/1.

You can check which processes are currently linked to a process (and still running) with Process.info/1.

pid = spawn_link(fn -> :timer.sleep(50_000) end)
# => #PID<0.126.0>

self()
|> Process.info()
|> Keyword.get(:links)

# => [#PID<0.126.0>]

Linking processes can be useful when doing parallelized work when each chunk of work shouldn't be continued in case another chunk fails to finish.

Trapping exits

Linking can also be used for supervising processes. If a process traps exits, it will not crash when a process to which it's linked crashes. It will instead receive a message about the crash. This allows it to deal with the crash gracefully, for example by restarting the crashed process.

A process can be configured to trap exits by calling Process.flag(:trap_exit, true). Note that Process.flag/2 returns the old value of the flag, not the new one.

The current value of the flag can be checked using Process.info/1.

pid
|> Process.info()
|> Keyword.get(:trap_exit)

# => false

The message that will be sent to the process in case a linked process crashes will match the pattern {:EXIT, from, reason}, where from is a PID. If reason is anything other than the atom :normal, that means that the process crashed or was forcefully killed.

parent_pid =
  spawn(fn ->
    Process.flag(:trap_exit, true)

    spawn_link(fn -> raise "oops" end)

    receive do
      message ->
        IO.inspect(message, label: "received message")

        # do something else,
        # to demonstrate that the process stays alive
        :timer.sleep(50_000)
    end
  end)

# => 20:05:34.125 [error] Process #PID<0.295.0> raised an exception
#    ** (RuntimeError) oops
#    (stdlib 3.13.2) erl_eval.erl:678: :erl_eval.do_apply/6

# received message: {:EXIT, #PID<0.295.0>,
#   {%RuntimeError{message: "oops"},
#     [{:erl_eval, :do_apply, 6, [file: 'erl_eval.erl', line: 678]}]}}

Process.alive?(parent_pid)
# => true

Note that trapping exits makes the process immune to crashes in all of its linked processes, and exit messages might accumulate in the process's mailbox if left unread. For this reason, you should keep track of all the processes linked to a process that traps exits. Usually, processes that trap exits are only responsible for supervising other processes and don't do anything else.

In practice, you wouldn't implement a supervisor process from scratch yourself. Elixir provides an abstraction for this kind of functionality in the form of the Supervisor behaviour.

Other abstractions around processes, like Agent, GenServer, or Task, provide two ways of starting them - either start or start_link.

Edit via GitHub The link opens in a new window or tab

Learn Links