When Processes Die

Who gets told when a process dies? By default, no one. Obviously the VM knows and can report it to the console, but your code will be oblivious unless you explicitly tell Elixir you want to get involved. Here’s the default case: we spawn a function that uses the Erlang timer library to sleep for 500 ms. It then exits with a status of :boom. The code that spawns it sits in a receive. If it receives a message, it reports that fact; otherwise, after one second it lets us know that nothing happened.

spawn/link1.exs
 defmodule​ Link1 ​do
 import​ ​:timer​, ​only:​ [ ​sleep:​ 1 ]
 
 def​ sad_function ​do
  sleep 500
 exit​(​:boom​)
 end
 def​ run ​do
  spawn(Link1, ​:sad_function​, [])
 receive​ ​do
  msg ->
  IO.puts ​"​​MESSAGE RECEIVED: ​​#{​inspect msg​}​​"
 after​ 1000 ->
  IO.puts ​"​​Nothing happened as far as I am concerned"
 end
 end
 end
 
 Link1.run

(Think about how you’d have written this in your old programming language.)

We can run this from the console:

 $ ​​elixir​​ ​​-r​​ ​​link1.exs
 Nothing happened as far as I am concerned

The top level got no notification when the spawned process exited.

Linking Two Processes

If we want two processes to share in each other’s pain, we can link them. When processes are linked, each can receive information when the other exits. The spawn_link call spawns a process and links it to the caller in one operation.

spawn/link2.exs
 defmodule​ Link2 ​do
 import​ ​:timer​, ​only:​ [ ​sleep:​ 1 ]
 
 def​ sad_function ​do
  sleep 500
 exit​(​:boom​)
 end
 def​ run ​do
» spawn_link(Link2, ​:sad_function​, [])
 receive​ ​do
  msg ->
  IO.puts ​"​​MESSAGE RECEIVED: ​​#{​inspect msg​}​​"
 after​ 1000 ->
  IO.puts ​"​​Nothing happened as far as I am concerned"
 end
 end
 end
 
 Link2.run

The runtime reports the abnormal termination:

 $ ​​elixir​​ ​​-r​​ ​​link2.exs
 ** (EXIT from ​#PID<0.73.0>) :boom

So our child process died, and it killed the entire application. That’s the default behavior of linked processes—when one exits abnormally, it kills the other.

What if you want to handle the death of another process? Well, you probably don’t want to do this. Elixir uses the OTP framework for constructing process trees, and OTP includes the concept of process supervision. An incredible amount of effort has been spent getting this right, so I recommend using it most of the time. (We cover this in Chapter 18, OTP: Supervisors.)

However, you can tell Elixir to convert the exit signals from a linked process into a message you can handle. Do this by trapping the exit.

spawn/link3.exs
 defmodule​ Link3 ​do
 import​ ​:timer​, ​only:​ [ ​sleep:​ 1 ]
 
 def​ sad_function ​do
  sleep 500
 exit​(​:boom​)
 end
 def​ run ​do
» Process.flag(​:trap_exit​, true)
  spawn_link(Link3, ​:sad_function​, [])
 receive​ ​do
  msg ->
  IO.puts ​"​​MESSAGE RECEIVED: ​​#{​inspect msg​}​​"
 after​ 1000 ->
  IO.puts ​"​​Nothing happened as far as I am concerned"
 end
 end
 end
 
 Link3.run

This time we see an :EXIT message when the spawned process terminates:

 $ ​​elixir​​ ​​-r​​ ​​link3.exs
 MESSAGE RECEIVED: {:EXIT, ​#PID<0.78.0>, :boom}

It doesn’t matter why a process exits—it may simply finish processing, it may explicitly exit, or it may raise an exception—the same :EXIT message is received. Following an error, however, it contains details of what went wrong.

Monitoring a Process

Linking joins the calling process and another process—each receives notifications about the other. By contrast, monitoring lets a process spawn another and be notified of its termination, but without the reverse notification—it is one-way only.

When you monitor a process, you receive a :DOWN message when it exits or fails, or if it doesn’t exist.

You can use spawn_monitor to turn on monitoring when you spawn a process, or you can use Process.monitor to monitor an existing process. However, if you use Process.monitor (or link to an existing process), there is a potential race condition—if the other process dies before your monitor call completes, you may not receive a notification. The spawn_link and spawn_monitor versions are atomic, however, so you’ll always catch a failure.

spawn/monitor1.exs
 defmodule​ Monitor1 ​do
 import​ ​:timer​, ​only:​ [ ​sleep:​ 1 ]
 
 def​ sad_function ​do
  sleep 500
 exit​(​:boom​)
 end
 def​ run ​do
» res = spawn_monitor(Monitor1, ​:sad_function​, [])
  IO.puts inspect res
 receive​ ​do
  msg ->
  IO.puts ​"​​MESSAGE RECEIVED: ​​#{​inspect msg​}​​"
 after​ 1000 ->
  IO.puts ​"​​Nothing happened as far as I am concerned"
 end
 end
 end
 
 Monitor1.run

Run it, and the results are similar to the spawn_link version:

 $ ​​elixir​​ ​​-r​​ ​​monitor1.exs
 {​#PID<0.78.0>, #Reference<0.1328...>}
 MESSAGE RECEIVED: {:DOWN, ​#Reference<0.1328...>, :process,
  #PID<0.78.0>, :boom}

(The Reference record in the message is the identity of the monitor that was created. The spawn_monitor call also returns it, along with the PID.)

So, when do you use links and when should you choose monitors?

It depends on your processes’ semantics. If the intent is that a failure in one process should terminate another, then you need links. If instead you need to know when some other process exits for any reason, choose monitors.