RudManusachi

RudManusachi

Why `throw/catch` is discouraged for control flow in favor of `with` special form?

Hi there :wave: !

This week at CodeBEAM I had a chance to meet an amazing crowd and participate in some interesting conversations. One of them was with more experienced with erlang folks discussing new maybe_expr in OTP-25. @max-au raised concerns about it which I initially didn’t get right.

I’ve been happily writing elixir and using with for years by now, thinking that’s the right way to handle cleanly the scenario of nested branching expressions with a single happy path. And for some reason I never questioned why throw/catch is reserved as some sort of a “last resort” for libs that don’t return :ok/:error tuples but throw in error case.[*]

Also as mentioned in EEP-0049#Elixir

Elixir has a slightly different semantic approach to error handling compared to Erlang. Exceptions are discouraged for control flow (while Erlang specifically uses throw for it), and the with macro is introduced

Example:

Here, is the synthetic example that hopefully will help to show the idea:
Say I want to put together a function that does multiple things with given argument:

defmodule Foo do
  def bar(num) do
    num
    |> double()
    |> subtract(num)
    |> divide(num)
    |> multiply(num)
  end

  defp double(num), do: 2 * num
  defp subtract(a, b), do: a - b
  defp divide(a, b), do: a / b
  defp multiply(a, b), do: a * b 
end

Often in real world some of these private functions are expected to return erroneous results.. often we wrap the result in :ok and :error tuples and use with special form.

Here is the example of the same module, but where each private function might return either :ok or :error tuple and parent function uses with to focus on happy path and still be aware of possible erroneous results:

defmodule Foo do
  @spec bar(number) :: {:ok, number} | {:error, :not_number | :zero_division}
  def bar(num) do
    with {:ok, double_num} <- double(num),
         {:ok, subtracted_num} <- subtract(double_num, num),
         {:ok, divided_num} <- divide(subtracted_num, num) do
      multiply(divided_num, num)
    end
  end

  defp double(x) do
    if is_number(x),
      do: {:ok, 2 * x},
      else: {:error, :not_number}
  end

  defp subtract(a, b) do
    if is_number(a) and is_number(b),
      do: {:ok, a - b},
      else: {:error, :not_number}
  end

  defp divide(_a, 0), do: {:error, :zero_division}

  defp divide(a, b) do
    if is_number(a) and is_number(b),
      do: {:ok, a / b},
      else: {:error, :not_number}
  end

  defp multiply(a, b) do
    if is_number(a) and is_number(b),
      do: {:ok, a * b},
      else: {:error, :not_number}
  end
end

However, we could have achieved the same behavior without need to wrap everything into “monadic” :ok/:error tuples, just throw when something goes wrong and let parent catch it. As a bonus we can again use pipes (though, that’s not the priority point at this moment).

defmodule Foo do
  
  @spec bar(number) :: number | :not_number | :zero_division  
  def bar(num) do
    num
    |> double()
    |> subtract(num)
    |> divide(num)
    |> multiply(num)
  catch
    error -> error
  end

  defp double(x) do
    if is_number(x),
      do: 2 * x,
      else: throw(:not_number)
  end

  defp subtract(a, b) do
    if is_number(a) and is_number(b),
      do: a - b,
      else: throw(:not_number)
  end

  defp divide(_a, 0), do: throw(:zero_division)

  defp divide(a, b) do
    if is_number(a) and is_number(b),
      do: a / b,
      else: throw(:not_number)
  end

  defp multiply(a, b) do
    if is_number(a) and is_number(b),
      do: a * b,
      else: throw(:not_number)
  end
end

Apparently, this style is used in Erlang quite often.

Now I’m really puzzled, why it is discouraged in elixir?


[*] elixir-lang.org | Getting Started / Try, catch and rescue / Throws

Those situations are quite uncommon in practice except when interfacing with libraries that do not provide a proper API

Most Liked

hst337

hst337

Now I’m really puzzled, why it is discouraged in elixir?

I am not sure about Elixir, but personally I can find these reasons where with is preferred to catch

  1. Dialyzer. Consider this code
defmodule Throwing do

  def f(x, y, z) do
    divide(divide(x, y), z)
  end

  defp divide(_, 0), do: throw :oops
  defp divide(x, y), do: x / y

end

defmodule With do

  def f(x, y, z) do
    with(
      {:ok, xy} <- divide(x, y),
      {:ok, xyz} <- divide(xy, z)
    ) do
      xyz
    else
      :this_clause_doesnt_handle_anything -> :error
    end
  end

  defp divide(_, 0), do: :error
  defp divide(x, y), do: {:ok, x / y}

end

Dialyzer will only warn us about with, but it won’t warn about unhandled throw :oops

lib/sample.ex:1:pattern_match
The pattern can never match the type.

Pattern:
:this_clause_doesnt_handle_anything

Type:
:error

And the problem here is not just a dialyzer problem. Even a programmer won’t be able to figure out whether the thown value is handled somewhere up the callstack or is it just a develop who forgot to write a proper catch.

  1. Debugging
    Let’s assume, we don’t use dialyzer and we forgot to write catch clause to handle throw. The process will just fail, and all we’ll have is stacktrace of this throw. Now, with with we’d also have a place where we failed the matching, which will provide us an information on what was expected against what was received in match.

  2. Performance
    throw collects stacktrace information when it traverses the stack in search of the matching catch clause. with doesn’t do that

See the benchmark:

defmodule Throwing do

  def f(x1, x2, x3, x4, x5) do
    {:ok, divide(divide(divide(divide(x1, x2), x3), x4), x5)}
  catch
    :oops -> :error
  end

  defp divide(_, 0), do: throw :oops
  defp divide(x, y), do: x / y

end

defmodule With do

  def f(x1, x2, x3, x4, x5) do
    with(
      {:ok, r} <- divide(x1, x2),
      {:ok, r} <- divide(r, x3),
      {:ok, r} <- divide(r, x4),
      {:ok, r} <- divide(r, x5)
    ) do
      {:ok, r}
    else
      :error -> :error
    end
  end

  defp divide(_, 0), do: :error
  defp divide(x, y), do: {:ok, x / y}

end

Benchee.run(%{
  "throw" => fn ->
    :error = Throwing.f(1, 2, 3, 4, 0)
  end,
  "with" => fn ->
    :error = With.f(1, 2, 3, 4, 0)
  end
}, [
  warmup: 1,
  time: 5,
  memory_time: 2
])

"""
Operating System: Linux
CPU Information: 11th Gen Intel(R) Core(TM) i7-1185G7 @ 3.00GHz
Number of Available Cores: 8
Available memory: 15.35 GB
Elixir 1.13.4
Erlang 24.3.4.5

Benchmark suite executing with the following configuration:
warmup: 1 s
time: 5 s
memory time: 2 s
parallel: 1
inputs: none specified
Estimated total run time: 16 s

Benchmarking throw...
Benchmarking with...

Name            ips        average  deviation         median         99th %
with        10.11 M       98.90 ns ±39076.74%          29 ns          90 ns
throw        8.27 M      120.93 ns ±24530.51%          61 ns         170 ns

Comparison:
with        10.11 M
throw        8.27 M - 1.22x slower +22.02 ns

Memory usage statistics:

Name     Memory usage
with            120 B
throw           120 B - 1.00x memory usage +0 B
"""
dimitarvp

dimitarvp

Most of the exception workflows in the programming languages I worked with are frowned upon because they pass the responsibility for error handling in their own code to upstream callers. Which would be fine – if it was more visible.

(Java has checked exceptions that can be appended to the function signatures e.g. public void do_stuff_with_file(String path) throws IOException which helps but I’ve noticed many former colleagues have been outright ignoring those.)

So #1 reason for me is: code must be obvious through and through, 100%, including any and all errors it can return.

Code must not have surprising effects. Code should be “what you see is what you get”.

hauleth

hauleth

Actually throw isn’t for exceptions, it is exactly meant as an early return mechanism.

Where Next?

Popular in Discussions Top

matthias_toepp
I’d love to hear what people think about Wisp, the new Gleam web framework started by Gleam’s primary creator Louis Pilfold. Gleam, alon...
New
Nvim
Anybody knows a comprehensive comparison of Django and Phoenix, thanks for the help. Where are they similar? Where do they differ the m...
New
Nvim
Elixir appears to be a superior language to Python. I don’t see any advantage of Python over Elixir. Are there any?
New
jer
I’ve been using umbrellas for a while, and generally started off (on greenfield projects at least) by isolating subapps based on clearly ...
New
tomekowal
Hey guys! I want to create a toy project that shows a chart of temperature over time and updates every 5 seconds. I feel LiveView is per...
New
scouten
I’m looking for a host for the server part of a small (personal) side project that I’m working on. It’s currently written in Node.js and ...
New
opsb
We’re considering our architecture from a viewpoint of scaling our traffic heavily over the next 6 months. Our current deployment is runn...
New
New
MarioFlach
Hello, I want to share a project I’ve been working on for a while: https://github.com/almightycouch/gitgud Background Some time ago I ...
New
Fl4m3Ph03n1x
Background A few days ago I was listening to The future of Elixir from Elixir Talks, with Dave Thomas (@pragdave ) and Brian Mitchell. I...
New

Other popular topics Top

vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
Nvim
Anybody knows a comprehensive comparison of Django and Phoenix, thanks for the help. Where are they similar? Where do they differ the m...
New
JeremM34
Hello, how can I check the Phoenix version ? Thanks !
New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? Ecto.Repo — Ecto v3.14.0 has exampl...
New
belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
saif
Hello everyone, Long time lurker first time poster here. I’ve recently begun working on Elixir full-time again! :raised_hands: It’s been...
New
boundedvariable
I am going through the kafka architecture. All the features what the kafka is providing are already in Erlang. I would like hear your opi...
New
rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list. ...
New

We're in Beta

About us Mission Statement