Error handling in Elixir: rescue vs catch

Tyler Pachal
3 min readJun 25, 2020

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:

Using catch/2 to handle any kind of error

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:

Example of replacing rescue and catch/1 with catch/2

For a full table of scenarios and code snippets, head over to the GitHub repo I setup:

Screenshot of GitHub repo
GitHub repo which contains table of triggers, handlers, and their compatibility

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:

Example use-case for catch/2 where you want to handle errors without bubbling them up