An Introduction to Exceptions in Elixir
Tags: elixir
Published: 2023-10-03 11:00:00

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`](https://hexdocs.pm/elixir/1.14/Kernel.html#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](https://appsignal.com/users/sign_up) 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](https://elixir-lang.org/getting-started/mix-otp/supervisor-and-application.html) 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](https://www.appsignal.com/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](https://blog.appsignal.com/images/blog/2023-06/trace-sample.png)

Check out the [AppSignal for Elixir installation guide](https://docs.appsignal.com/elixir/installation.html) 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](/elixir-alchemy)!**