Error handling in Elixir: rescue vs catch
Cutting to the chase: rescue/1
and catch/1
(rescue
and catch
with one parameter) handle different types of errors; to handle all kinds of errors use catch/2
(the version of catch
with two parameters) .
See the GitHub README for complete examples, and there is additional information in the docs.
Error handling (aka exception handling) in Elixir often manifests itself with the try
and rescue
keywords, which correspond to errors that are triggered using raise/1
and raise/2
. But in addition to raise
there are other functions for triggering errors:
throw/1
exit/1
:erlang.error/1
(Talking about what each of these are for is outside the scope of this blog post)
In Elixir these functions are not as common as raise
and you may not have them in your application code— but this does not mean they are not in your project. The exit
and :erlang.error
are quite rare but throw
is not uncommon among Erlang dependencies and Elixir code.
How do each of these functions interact with rescue/1
? I have assembled a GitHub repo which maps out each scenario in more details, but the summary is that if you want to handle any kind of error that might occur you should use catch/2
.
Here is an example of using catch/2
in your code:
Doing this will handle any error triggered by raise
, throw
, :erlang.error
, and exit
.
The kind
parameter is used to represent which function triggered the error, and will be one of :error
, :throw
, :error
, or :exit
respectively (the :error
atom is returned for raise
and for :erlang.error
).
Here are some examples of equivalent error handling:
For a full table of scenarios and code snippets, head over to the GitHub repo I setup:
Using catch/2
everywhere is not always necessary, in fact, rescue/1
provides some functionality which catch/2
does not:
- Translate Erlang errors into Elixir errors (these are errors triggered from
:erlang.error
) - Rescuing an error by its name without needing to bind to a variable
Not handling errors and letting a supervisor do-its-thing is also an idiomatic way to deal with errors in Elixir. However, if you have a deep call stack and want to handle any error that may arise, catch/2
is the way to go.
In my case, we have an application which consumes from Kafka and must not get stuck on messages which cannot be processed. We use the Dead Letter Queue pattern where we store non-processable-messages whenever something goes wrong.
To achieve this in Elixir, we have a top-level catch/2
clause which handles any error which arises and sends the messages to the DLQ to unblock the messages behind it. Any failures in sending to the DLQ will bubble up to the supervisor. Something like this: