1

In erlang, there is only throw, no raise. What's the difference between raise and throw in elixir?

error produced code is as follows:

  defp open_imu() do
    {:ok, pid} = Circuits.UART.start_link()
    # imu_port: "tty.usbserial-1410", 
    imu_port = Application.fetch_env!(Mechanics, :imu_port)
    imu_speed = Application.fetch_env!(Mechanics, :imu_speed)
    case Circuits.UART.open(pid, imu_port, speed: imu_speed, active: true) do
      :ok ->
       pid;
       {:error,reason} ->
        Logger.error("serial can not open")
         throw(reason) # <----- if use throw, it is ok, if use raise, it is not ok.
    end
  end

When using raise, iex produce the following message, it can't be catched.

03:12:05.921 [error] serial can not open
 
03:12:05.923 [error] GenServer Mechanics.MechanicsImu terminating
** (UndefinedFunctionError) function :enoent.exception/1 is undefined (module :enoent is not available)
    :enoent.exception([])
    (mechanics 0.1.0) lib/mechanics/mechanics_imu.ex:91: Mechanics.MechanicsImu.open_imu/0
    (mechanics 0.1.0) lib/mechanics/mechanics_imu.ex:35: Mechanics.MechanicsImu.handle_info/2
    (stdlib 3.17.2) gen_server.erl:695: :gen_server.try_dispatch/4
    (stdlib 3.17.2) gen_server.erl:771: :gen_server.handle_msg/6
    (stdlib 3.17.2) proc_lib.erl:226: :proc_lib.init_p_do_apply/3
Last message: :check_equipment
State: %{imuPid: nil, interval: 5000}
Chen Yu
  • 3,955
  • 1
  • 24
  • 51
  • 2
    Does this answer your question? [Elixir - try/catch vs try/rescue?](https://stackoverflow.com/questions/40280887/elixir-try-catch-vs-try-rescue) – Adam Millerchip Jul 20 '22 at 05:22

2 Answers2

2

throw is more like flow control and can can be seen as an early return, where you can throw any value type up out of scope to something else and halting the current function.

raise is specifically for exceptions, where something failed, an error. You can only raise exceptions, but there are some syntax lipstick to bundle strings into an exception.

You cant raise an atom (Unless the atom is the name of a module that defines an exception/1 function.

iex(local@host)1> raise :some_atom
** (UndefinedFunctionError) function :some_atom.exception/1 is undefined (module :some_atom is not available)
    :some_atom.exception([])

Raising some random data gives a clearer error about the allowed types:

iex(local@host)1> raise %{}
** (ArgumentError) raise/1 and reraise/2 expect a module name, string or exception as the first argument, got: %{}

Raising a string is syntactic sugar around a RuntimeError:

iex(local@host)1> raise "runtime error shortcut"
** (RuntimeError) runtime error shortcut

Raise the runtime struct directly:

iex(local@host)1> raise %RuntimeError{}
** (RuntimeError) runtime error

The module alone also works:

iex(local@host)1> raise RuntimeError
** (RuntimeError) runtime error

Because technically it's raising this atom:

iex(local@host)1> raise :"Elixir.RuntimeError"
** (RuntimeError) runtime error

And the counter point, throw can accept any value:

iex(local@host)1> throw 10
** (throw) 10

iex(local@host)1> throw :atom
** (throw) :atom

iex(local@host)1> throw nil
** (throw) nil

It's worth pointing out that throw is "inlined by the compiler" and dramatically faster than raising which has a bit more brains and needs to create structs, etc. I would argue that the difference is probably moot though as you shouldn't be raising without reason and there are generally better patterns than throwing, but in some cases this will be impactful.

Generated throw_vs_raise app
Operating System: Linux
CPU Information: Intel(R) Core(TM) i5-4670K CPU @ 3.40GHz
Number of Available Cores: 4
Available memory: 23.37 GB
Elixir 1.13.1
Erlang 24.2

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

Benchmarking raise ...
Benchmarking throw ...

Name            ips        average  deviation         median         99th %
throw        6.24 M       0.160 μs  ±4661.27%       0.156 μs       0.190 μs
raise        0.91 M        1.10 μs    ±43.02%        1.07 μs        1.48 μs

Comparison:
throw        6.24 M
raise        0.91 M - 6.84x slower +0.94 μs
Benchee.run(
  %{
    "throw" => fn ->
      try do
        throw "throw"
      catch
        t -> t
      end
    end,
      "raise" => fn ->
        try do
          raise "raise"
        rescue
          r -> r
        end
      end})

You could, if mad enough, bundle data inside an exception and use that for flow control but it's not recommended, but to that point they both share some very similar behaviour where you halt the current execution and pass a value up the stack to somewhere else, but the semantics of what and why you're passing that value up is different.

1

In elixir raise is used for raising exceptional situations in the code e.g.

iex> :foo + 1
** (ArithmeticError) bad argument in arithmetic expression: :foo + 1
    :erlang.+(:foo, 1)

To catch a raised value you use rescue, like this

iex> try do
...>     raise "something went wrong..."
...> rescue
...>     RuntimeError -> "Got an error!"
...> end
"Got an error!"

throw has a slightly different purpose - it is used when you (for some reason) have to throw a value to catch it later. Thrown doesn't necessarily mean that something exceptional happened in the code. It is used quite rarely, usually when you work with a library that doesn't provide a proper API

To catch a thrown value you use catch

iex> try do
...>     throw "my cool value"
...> catch
...>    x -> "Received #{x}"
...> end
"Received my cool value"

Check getting started, everything is described there

kraleppa
  • 11
  • 3