W0052 - Avoid catch
Warning
-module(main).
-export([catcher/2]).
catcher(X,Y) ->
case catch X/Y of
%% ^^^^^ warning: Avoid `catch`.
{'EXIT', {badarith,_}} -> "uh oh";
N -> N
end.
Explanation
try ... catch ... end
in Erlang has been available for a very long time, and
the old, simplistic catch Expr
will be dropped from the language. In most
places where it is used, it carries with it some unwanted (and often unknown)
corner case behaviour, such as discarding the original error location,
conflating errors, exits, and throws, and mixing up thrown values with normally
returned values in a way that makes it very hard to see what the author
intended, and whether the implementation really does what they hoped.
Maintainance of such code is very hard.
Starting with Erlang OTP 28, it will be possible to enable warnings for use of
old-style catch
expressions via the warn_deprecated_catch
flag. The
old-style catch will be deprecated and removed in a future version of OTP (most
likely OTP 29 or OTP 30). Therefore, it is recommended to avoid switching to
try ... catch ... end
.
Examples
Let's consider the following example:
case catch api() of
{'EXIT', Reason} ->
caught_it;
_ ->
other
end.
First notice that this always returns other
if api()
calls throw(...)
because foo = (catch throw(foo))
. We only get something interesting if api()
calls exit(...)
or error(...)
. So, if your goal was to do something special
when there is an exception, case catch
is definitely not what you want.
If you only want to catch exits and errors, it might work, unless the API
legitimately returns a tuple with first element being 'EXIT'
. So, this pattern
is brittle and does not generalize. Here is a better way:
try api() of
_ ->
other
catch
_:_ ->
caught_it
end.
It is only one line longer than case catch
, does not miss throw()
s, has less
runtime overhead, has 15 fewer characters, and never trips on special return
values of functions.
One case where using try/catch
is more cumbersome is if you want to treat
exceptions and unexpected return values the same. In that case you may want to
do:
case catch api() of
expected -> continue_processing();
Other -> handle_unexpected(Other)
end.
However, if api()
calls throw(expected)
it gets treated the same as if it
returns expected. This makes the intended interface of api()
unclear. So, it
is still recommended to use try/catch
for this case:
try api() of
expected -> continue_processing();
Other -> handle_unexpected(Other)
catch
Type:Reason -> handle_unexpected(Type, Reason)
end.
It is more verbose but makes the interface and control flow clear.
Preserving the old behaviour
When converting a catch
into a try ... catch ... end
, keep in mind that we
may rely on the result of the catch
expression itself.
As an example, it would be ok to replace:
sum(X, Y) ->
catch could_crash(X),
X + Y.
With:
sum(X, Y) ->
try could_crash(X)
catch
_:_ ->
ok
end,
X + Y.
This is because the code inside the catch
(then converted into a
try ... catch ... end
) is only used for its side effects, and the result of
the could_crash/1
function is ignored.
Things are different in the following example:
main(X) ->
catch could_crash(X).
could_crash(42) ->
throw(crash);
could_crash(_X) ->
ok.
One could mechanically convert the catch
into a try ... catch
:
main(X) ->
try could_crash(X)
catch _:_ ->
ok
end.
could_crash(42) ->
throw(crash);
could_crash(_X) ->
ok.
This conversion could be potentially dangerous and should be rethinked, since
the old behaviour is not preserved. In fact, when calling my_function(42)
, the
old code returns crash
, while the new code returns ok
.
By examining the surrounding code you should be able to assess whether a potentially backward incompatible behaviour is acceptable or not.