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.