elixir

An Introduction to Exceptions in Elixir

Pulkit Goyal

Pulkit Goyal on

An Introduction to Exceptions in Elixir

Exceptions are a core aspect of programming, and a way to signal when something goes wrong with a program. An exception could result from a simple error, or your program might crash because of underlying constraints. Exceptions are not necessarily bad, though — they are fundamental to any working application.

Let’s see what our options are for handling exceptions in Elixir.

Raising Exceptions in Elixir

The Elixir (and Erlang) community is generally quite amenable to exceptions: “let it crash” is a common phrase. This is due, in part, to the excellent OTP primitives in Erlang/Elixir. The OTP primitives allow us to create supervisors that manage and restart processes (or a group of related processes) on failure.

Exceptions can occur when something unexpected happens in your Elixir application. For example, division by zero raises an ArithmeticError.

elixir
iex> 1 / 0 ** (ArithmeticError) bad argument in arithmetic expression: 1 / 0 :erlang./(1, 0) iex:1: (file)

Exceptions can also be raised manually:

elixir
iex> raise "BOOM" ** (RuntimeError) BOOM iex:1: (file)

By default, raise creates a RuntimeError. You can also raise other errors by using raise/2:

elixir
defmodule Math do def div(a, b) do if b == 0, do: raise ArgumentError, message: "cannot divide by zero" a / b end end iex> Math.div(1, 0) ** (ArgumentError) cannot divide by zero iex:4: Math.div/2 iex:3: (file)

A function call that doesn’t match a defined function raises an ArgumentError by default:

elixir
defmodule Math do def div(a, b) when b != 0 do a / b end end iex> Math.div(1, 0) ** (FunctionClauseError) no function clause matching in Math.div/2 The following arguments were given to Math.div/2: # 1 1 # 2 0

Handling Elixir Exceptions

Elixir provides the try-rescue construct to handle exceptions:

elixir
try do raise "foo" rescue e in RuntimeError -> IO.inspect(e) end

This prints %RuntimeError{message: "foo"} in the console but doesn’t crash anything. Use this when you want to recover from exceptions in Elixir. It is also possible to skip binding the variable if it is unnecessary. For example:

elixir
try do raise "foo" rescue RuntimeError -> IO.puts("something bad happened") end

In both of the above examples, we only rescue RuntimeError. If there is another error, it will still raise an exception. To rescue all exceptions raised inside the try block, use the rescue without an error type, like this:

elixir
try do 1 / 0 rescue e -> IO.inspect(e) end

Running the above prints %ArithmeticError{message: "bad argument in arithmetic expression"}. If you want to perform different actions based on different exceptions, just add more clauses to the rescue branch.

elixir
try do 1 / 0 rescue RuntimeError -> IO.puts("Runtime Error") ArithmeticError -> IO.puts("Arithmetic Error") _e -> IO.puts("Unknown error") end

While rescuing all errors (without using a specific type) sounds tempting, it is a good practice to rely on specific exceptions because:

  1. You can perform different recovery tasks for different exceptions.
  2. It saves you from future breaking changes or programming errors going unnoticed.

For example, consider the below function:

elixir
defmodule Config do def get(file) do try do contents = File.read!(file) parse!(contents) rescue _e -> %{some: "default config"} end end end

It reads and parses a config file, returning the result. When written, it uses a generic clause to rescue from missing file-related errors. Let’s say that a while later, the input file gets corrupted somehow (e.g., a simple missing } or an extra , in a JSON file), and now there are parsing errors. Our function will still work without raising any issues, and we will be left guessing why it doesn’t work even though there’s an existing file.

Another common practice in the Elixir community is to use ok/error tuples to signal errors instead of raising exceptions (for both internal and external libraries). So, instead of returning a simple result or raising an exception on an issue, a function in Elixir will return {:ok, result} on success and {:error, reason} on failure.

This means that most of the time, a case block will be preferable to try/rescue. For example, the above File.read! can be replaced by:

elixir
case File.read(file) do {:ok, contents} -> parse!(contents) {:error, reason} -> %{some: "default config"} end

In practice, you should reach out for try/raise only in exceptional cases, never as a means of control flow. This is quite different from some other popular languages like Ruby or Java, where unexpected operations usually raise an error, then handle control flow for cases like non-existent files.

Re-raising Exceptions

Sometimes, we just want to know that there is an exception but not rescue from it — for example, to log an exception before allowing the process to crash or to wrap the exception into something more useful/understandable to the user.

This is where reraise can be helpful, as it preserves an exception's existing stack trace:

elixir
try do 1 / 0 rescue e -> IO.puts(Exception.format(:error, e, __STACKTRACE__)) reraise e, __STACKTRACE__ end

Here, we use the __STACKTRACE__ to retrieve the original trace of the exception, including its origin and the full call stack.

In a real-world application, you might use this to report a metric / send an exception to a third-party exception tracking service, or log something informative to a third-party logging service. For example, you might send telemetry data to AppSignal under these circumstances.

In addition, it is also common practice to have custom exceptions for cases where unexpected things happen in libraries. reraise/3 can be useful here:

elixir
defmodule DivisionByZeroError do defexception [:message] end defmodule Math do def div(a, b) do try do a / b rescue e in ArithmeticError -> reraise DivisionByZeroError, [message: e.message], __STACKTRACE__ end end end

Now using Math.div(1, 0) will raise a DivisionByZeroError instead of a generic ArithmeticError.

Rescue And Catch in Elixir

In Elixir, try/raise/rescue and try/throw/catch are different. raise and rescue are used for exception handling, whereas throw and catch are used for control flow.

A throw stops code execution (much like a return — the difference being that it bubbles up until a catch is encountered). It is rarely needed and is only an escape hatch to be used when an API doesn’t provide an option to do something.

For example, let's say that there’s an API that produces numbers and invokes a callback:

elixir
defmodule Producer do def produce(callback) do Enum.each((1..100) , fn x -> callback.(x) end) end end

(This is just an example. Assume that with the real API, it is much more expensive to produce each number, and it produces an infinite list of numbers.)

We want to find the first number divisible by 13, since we don’t have any other apparent way of finding that number and stopping the production at that point. Let’s see how we can use throw/catch to do this:

elixir
try do Producer.produce(fn x -> if rem(x, 13) == 0 do throw(x) end end) catch x -> IO.puts("First number divisible by 13 is #{x}") end

It's worth stating that this is a bit of a contrived case, and most good APIs are designed in such a way that you never need to reach out for throw/catch.

After and Else Blocks in Elixir

You can use after and else blocks with all try blocks. An after block is called after processing all other blocks related to the try, regardless of whether any of the blocks raised an error. This is useful for performing any required clean-up operations.

For example, the below code creates a new producer and makes some results. If there’s an error during production, it will raise an exception. But the after block ensures that the producer is still disposed of regardless.

elixir
producer = Producer.new try do Producer.produce!(producer) after Producer.dispose(producer) end

On the other hand, an else block is called only if the try block is completed without raising an error. The else block receives the try block's result. The return value from the else block is the final return value of the try. For example:

elixir
try do File.read!("/path/to/file") rescue File.Error -> :not_found else "a" -> :a _other -> :other end

In the above code, if the file exists with content a, the result will be :a. The result is :not_found if the file doesn’t exist, and :other if the content is anything else.

Exceptions and Processes

No discussion about Elixir exceptions is complete without examining their impact on processes. Any unhandled exceptions cause a process to exit.

With this in mind, let’s revisit the “let it crash” strategy. If we don’t handle an exception from a process, it will crash. This is good in a way because:

  1. Since all processes are separate from each other, an exception or unhandled crash from one process can never affect the state of another process.
  2. In most sophisticated Elixir applications, all the processes run under a supervision tree. So, an unexpected exit will restart the process (depending on the supervision strategy) and any linked processes with a clean slate.

In most cases, intermittent issues will resolve themselves in the next run. This is much easier (and usually also cleaner) than handling each failure separately and performing a recovery step.

If you want to learn more about supervisors, I suggest the official Elixir Supervisor and Application guide as a great starting point.

Monitoring Exceptions

As we've seen, exceptions are a fundamental part of any application. Handling them is good, but sometimes, letting them crash a process is even better.

But in all cases, it is better to monitor exceptions happening in the real world so that you can take action if there’s something concerning (for example, a developer error that crashes an app).

This is where AppSignal for Elixir can help. Once set up, it automatically records all errors and can also trigger alerts based on predefined conditions.

Here's an example of an individual error you can get to from the "Errors" -> "Issue list" in AppSignal for debugging:

Trace sample

Check out the AppSignal for Elixir installation guide to get started.

Wrapping Up

In this post, we explored how errors are treated in Elixir and how to recover from them. We also saw that sometimes it is better to just let a process crash and be restarted through the supervisor than to manually perform recovery steps for all possible exceptions.

Elixir’s API makes a clear distinction between functions that raise an exception (usually ending in !) and functions that return a success/error tuple. If you are a library author, it is better to provide both options for users.

Reach out for the tuple-based methods when you need to handle error cases separately. In Elixir, we rarely use try blocks for control flow — the ! functions are for when we want a process to crash on unexpected events.

Until next time, happy coding!

P.S. If you'd like to read Elixir Alchemy posts as soon as they get off the press, subscribe to our Elixir Alchemy newsletter and never miss a single post!

Pulkit Goyal

Pulkit Goyal

Our guest author Pulkit is a senior full-stack engineer and consultant. In his free time, he writes about his experiences on his blog.

All articles by Pulkit Goyal

Become our next author!

Find out more

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal
AppSignal monitors your apps