It is accidental design quirk. The thing is that whole “do-block” in def* functions is de facto try-block, so these two:
def test(a) do
try do
IO.inspect(a)
else
r -> IO.inspect("from else #{r}")
end
end
Is de facto the same as:
def test(a) do
IO.inspect(a)
else
r -> IO.inspect("from else #{r}")
end
I assume that Elixir uses such approach to convert the errors from Erlang errors to Elixir exceptions on function boundary in case if it is called from other BEAM language, but I am not 100% sure. In short, it is design decision that is there and we need to live with it at least till Elixir 2.
This approach is often used for early returns from functions using throw/1 and catch block in try:
def foo(a) do
# some computation
if discombobulated?(a), do: throw(:early_exit)
# more computations
{:ok, result}
catch
:early_exit -> {:error, {:discombobulated, a}}
end