Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Error Handling

_2024_08_22_elixir/error_handling.livemd

Error Handling

Errors

There are 3 types of errors in elixir, Errors, Throws and Exits.

Error are raised typically from things like division by 0, calling none existing functions, error in pattern matching.

Errors can be raised by using the marcro:

raise("Something went wrong")

Functions that raises errors should always end with ! as per convention.

Exits are an other type of Error that’s used to terminate a process.

spawn(
  fn ->
    exit("I'm done")
    IO.puts("This doesn't happen")
  end)

The text in the exits serves to provide more information when the Error is intercepted by an other process.

The last error type Throw is used non local returns. Elixir does not have any break, continue, and return in recursions, throw can be used inside a deep loop to stop in and catch the return up the stack, this is not recomended and should be avoided as much as possible.

Error handling

Error handling can be achieved with the usual try catch.

try do
  :erlang.phash2("hello world")
  raise "Something bad"
catch error_type, error_value ->
  IO.puts(error_type)
  IO.puts(error_value.message)
end
try_helper = fn fun ->
  try do
    fun.()
    IO.puts("No error.")
  catch type, value ->
    IO.puts(
      """
        Error
        #{inspect(type)}
        #{inspect(value)}
      """
    )
  end
  end
try_helper.(fn -> raise("Something went wrong") end)
try_helper.(fn -> throw("Thrown value") end)
try_helper.(fn -> exit("I'm done") end)

Raise returns a struct RuntimeError while throw and exit returns it’s term like it is.

Try catch block also return values like normal funtions.

result =
  try do
    throw("Thrown value")
  catch type, value -> {type, value}
  end

An after keyword can be used for a code block to excecute in both success and failure

try do
  raise("Something went wrong")
  catch
    _,_ -> IO.puts("Error caught")
  after
    IO.puts("Cleanup code")
end

Tail recursion cannot be achieved with try catch block because a value is always waited to return.

Concurrency errors

Calling spawn will create a new process without any link to the other any error in one cannot be catched in the other.

Callink spawn_linked with create a linked process, any error in one will result in both process stopping.

There are some ways to keep the other process from closing

spawn(
  fn ->
  Process.flag(:trap_exit, true)
  spawn_link(
    fn -> raise("Something went wrong") end)
      receive do
        msg -> IO.inspect(msg)
      end
  end)

Process.flag(:trap_exit, true) will make sure the calling process won’t close when liked process have errors. instead the error is sent as a message in mailbox

Monitors

a process can start monitoring an other process or listening for errors

target_pid = spawn(
  fn ->
    Process.sleep(1000)
  end
)
Process.monitor(target_pid)
receive do
  msg -> IO.inspect(msg)
end

Supervisors

Supervisors as processes that manages the lifesycle of other processes, they can start processes that are it’s childrens and manage them using links, monitors, and exit traps.

A supervisor start a child process and traps it’s exits, if the child terminates the supervisor recieves a message and handle it acordingly, restart the child or something else. If the supervisor is terminated, all child processes are terminated as well.

There are two ways to start a supervisor, basic one is using Supervisor.start_link and the other is using a module.

Discovering processes through a registry

Sometimes when having multiple process linked to a supervisor you’d want to get all the runing processes ids. if you list processes on creation and one of them is terminated the list is no longer valid, to remedy this we use registeries.

When a process starts it calls a funtions that adds it to the registery and since the registery is liked to the processes if one of them terminates it will be notified and the process will be removed from the registery.

Initiating a process as a registery:

 Registry.start_link(name: :my_registry, keys: :unique)

name takes an atom, this atom is used to call funtions on the registery. The key is used for the type of registery (:unique or :duplicate)

Now that the registery is created, it’s time to create a process and linking it to the registery.

spawn(
  fn ->
    Registry.register(:my_registry, {:database_worker, 1}, nil)
    receive do
      msg -> IO.puts("got message #{inspect(msg)}")
    end
  end
)

Now to discover the processes linked to the registery:

[{db_worker_pid, value}] = Registry.lookup(:my_registry, {:database_worker, 1})
send(db_worker_pid, :some_message)

Now because the process terminates itself after recieving a message, it should not be in the registery anymore.

Registry.lookup(:my_registry, {:database_worker, 1})

Via tuples

Via tuple is an important concept in elixir, it provide a convenient way to register process into registery via third part process.

defmodule EchoServer do
  use GenServer
  def start_link(id) do
    GenServer.start_link(__MODULE__, nil, name: via_tuple(id))
  end
  def init(_), do: {:ok, nil}
  def call(id, some_request) do
    GenServer.call(via_tuple(id), some_request)
  end
  defp via_tuple(id) do
    {:via, Registry, {:my_registr, {__MODULE__, id}}}
  end
  def handle_call(some_request, _, state) do
    {:reply, some_request, state}
  end
end

when calling Genserve.start_link/3 the third argument is a via tuple wich is

Registry.start_link(name: :my_registr, keys: :unique)
EchoServer.start_link("server one")
EchoServer.start_link("server two")
EchoServer.call("server one", :some_request)
EchoServer.call("server two", :another_request)

A registery can use arbitrarly complex term to register processes