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