Handling Errors

Elixir has 3 varieties of errors.

  • Exceptions
  • Throws
  • Exits

Each type serves a different purpose. Before we dig in, it is worth mentioning that these are used far less frequently in Elixir than in other languages. Expect that your usage of these will also be “exceptional”, meaning infrequent and only when required.

Exceptions

Exceptions are not intended for managing Code Flow or control flow. They are intended for actual errors. Here’s an example of a ArithmeticError raised exception.

:customer + 10
#=> ** (ArithmeticError) bad argument in arithmetic expression: :customer + 10
#=>     :erlang.+(:customer, 10)

Raising an exception

You can raise a RuntimeError using the raise keyword.

raise "something failed!"
#=> ** (RuntimeError) something failed!

You can also raise a specific error type using raise/2. You provide the exception name/type and a message. Other attributes may be defined on an exception and using a keyword list you can set those as well.

raise ArgumentError, "you must provide a valid arg"
#=> ** (ArgumentError) you must provide a valid arg

raise ArgumentError, message: "you must provide a valid arg"
#=> ** (ArgumentError) you must provide a valid arg

Defining custom exceptions

You can create custom exceptions as well. They are defined within a module this way:

defmodule MySpecialError do
  defexception message: "Something special blew up", extra: nil
end

# raising with default message
raise MySpecialError
#=> ** (MySpecialError) Something special blew up

# overriding the message
raise MySpecialError, "what just happened?"
#=> ** (MySpecialError) what just happened?

# setting :extra value requires keyword usage
raise MySpecialError, message: "custom message", extra: 123
#=> ** (MySpecialError) custom message

The defexception works like a defstruct where you provide the keys that your exception will have and optionally any default values.

The :extra value is set in the MySpecialError but a simplified error representation is written to the console. In order to access that data or handle the error, we need to use try/rescue.

Handling with try/rescue

The try/rescue block is used to handle raised exceptions.

try do
  raise "Boom!"
rescue
  e in RuntimeError ->
    "Raised error: #{e.message}"
end
#=> "Raised error: Boom!"

If we wanted to handle the custom exception we defined before and access the data on the :extra field, we can do the following.

defmodule MySpecialError do
  defexception message: "Something special blew up", extra: nil
end

try do
  raise MySpecialError, message: "custom message", extra: 123
rescue
  e in MySpecialError ->
    IO.puts("Found MySpecialError.extra value #{e.extra}")
    {:error, "#{e.message}: #{e.extra}"}
end
#=> Found MySpecialError.extra value 123
#=> {:error, "custom message: 123"}

This example handles the raised custom error. The rescue block doesn’t support full pattern matching on the exception. It expects the expression to be a variable, a module, or a var in Module format. This still lets us get basic matching enough to access the :extra information on the error and transform it to an {:error, reason} tuple.

If you don’t care about the error type or even referencing the error, you can do this:

try do
  raise "Boom!"
rescue
  _ ->
    "Hit an error"
end
#=> "Hit an error"

The above matches on any exception type.

Specifying an exception’s module name, you can match on specific error type if needed.

try do
  raise "Boom!"
rescue
  RuntimeError ->
    "A runtime error was hit"

  _ ->
    "Hit an error"
end
#=> "A runtime error was hit"

Writing functions that raise exceptions

Sometimes you may want to create a function that raises an exception when something is really wrong. If your code explicitly raises exceptions, then the convention is to name it ending with a !. This is a naming convention used to convey the function may raise an exception. It typically means, “Perform the operation or raise an exception.” This is also called a “bang”. An example is the Enum.fetch!/2 function. You would read this as the “Enum fetch bang” function. From the documentation, it is described as:

Finds the element at the given index (zero-based). Raises OutOfBoundsError if the given index is outside the range of the enumerable.

https://hexdocs.pm/elixir/Enum.html#fetch!/2

Many of the functions in the File module have ! (bang) versions as a failure to read a file may be considered truly exceptional and a reason to fail entirely.

As mentioned previously, in Elixir it is far less common to deal with exceptions than what you find in other languages. For this reason, the File module also contains {:ok, result} and error responses for all the same functions. It is easier to pattern match and more common to respond to an :ok/:error tuple response than raised exceptions.

Alternate Syntax

An alternate syntax exists for the try/rescue block. Within a function, you can just use the rescue at the end of the function block without including the try.

defmodule Testing do
  def full_try do
    try
      # operation that might raise exception
    rescue
      # handling logic
    end
  end

  def without_try do
    # operation that might raise exception
  rescue
    # handling logic
  end
end

The above are equivalent. When your try/rescue block would encompass the full function body, you can use the abbreviated version. It is cleaner and has less indentation noise.

Exercise #1 – Conditionally raise an exception

The project has an existing function CodeFlow.Fake.Users.find/1. It finds a user and returns an {:ok, user} or {:error, reason} result. Your team wants you to create a find_user!/1 function that calls the existing Users.find/1 function but unwraps the tuples. When found, return the user not in a tuple. When not found, raise an exception where the message is the reason from the :error tuple.

The tests for this exercise are here:

mix test test/errors_test.exs:18

Exercise #2 – Transforming an exception

As your team has gotten more comfortable with Elixir and has started “thinking Elixir”, they realize they have too many functions raising exceptions. They brought habits from other languages with them when they were new to Elixir. Now they want to add more functions that return tuples but not break all the existing code using the exception versions.

The project has a function named CodeFlow.Fake.Orders.find!/1 that raises an exception when an order is not found. Your team wants you to create a CodeFlow.Errors.find_order/1 function that uses the existing CodeFlow.Fake.Orders.find!/1 function to do the work but transforms a raised exception into a tuple response. Capture the exception’s message and use that as the reason for the failure. When an order is found, wrap it in an {:ok, order} tuple.

The tests for this exercise are here:

mix test test/errors_test.exs:35

Throw and Catch

The throw and catch keywords are designed for Code Flow or control flow. It is reserved for situations where it is the only way you can get a value back. Again, like raise and rescue, throw and catch should be used sparingly.

The times where you might need to use throw and catch are when working with poorly designed libraries. So, truly, you don’t use these much. However, here’s what it looks like if you did need it.

This example stops at the first number evenly divisible by 17 but is not 17. It throws the matching value which is caught outside the loop.

try do
  Enum.each 1..1_000_000, fn(n) ->
    if rem(n, 17) == 0 && n != 17 do
      throw n
    end
  end
catch
  found -> "Caught #{found}"
end

I’ve never encountered an actual need for the throw keyword. You aren’t likely to either.

Exit

Elixir has a great concurrency story and that all happens because of green thread processes. We aren’t going to get into those at this point. However, this is relevant to an exit. An exit is a signal sent to the process running the code telling it to die.

exit("things went BOOM!")
#=> ** (exit) "things went BOOM!"

An exit signal is like a special thrown value which can be handled using catch.

try do
  exit "Abort! Abort!"
catch
  :exit, reason ->
    IO.inspect reason
    "Phew... caught that."
end
#=> "Abort! Abort!"
#=> "Phew... caught that."
Thinking Tip: Don’t actually catch exits

Exits deal with killing a process. In Elixir, processes are typically supervised. The exit is caught and handled by the supervisor who’s job is to restart a new process in a known good state to keep the system running smooth. Exit signals are an important part of a resilient system.

If using throw and catch are uncommon, then catching an exit is even less common! There may be cases where you want to trigger an exit, but you’ll be wanting a supervisor to handle it.

After

The after keyword is used to help ensure some cleanup operations happen either if the try block succeeds or blows up.

try do
  IO.puts "try block succeeded"
after
  IO.puts "after performed"
end
#=> try block succeeded
#=> after performed

try do
  raise "try block failed"
after
  IO.puts "after performed"
end
#=> after performed
#=> ** (RuntimeError) try block failed

An important point to realize about the after block is that it only offers a soft guarantee. The documentation on this is helpful to review.

Note that the process will exit as usual when receiving an exit signal that causes it to exit abruptly and so the after clause is not guaranteed to be executed. Luckily, most resources in Elixir (such as open files, ETS tables, ports, sockets, and so on) are linked to or monitor the owning process and will automatically clean themselves up if that process exits.

https://hexdocs.pm/elixir/Kernel.SpecialForms.html?#try/1-after-clauses

We learn that when a process accessing a resource dies, the resources linked to it are automatically closed. Typically in other languages you might use an after to close open files or resources. However, in Elixir you probably don’t need that case either. This means there aren’t likely to be many cases where you actually need an after.

Else

The else block is optional. If present, it provides the ability to pattern match on the result of the try block when it does not fail.

try do
  :success
rescue
  _ -> 
    :error
else
  :success ->
    "Yay, it worked!"
end
#=> "Yay, it worked!"

Note: any errors or failures in the else block code are not caught.

Variables and Scope

An important point to make with the try/rescue/catch/after blocks is that any variables bound inside those block do not leak out. For instance, when the code in the try block below fails, would you expect some variables to be bound but others not?

try do
  raise "boom!"
  status = "no error"
rescue
  _ ->
    status = "rescued"
end
status
#=> ** (CompileError) iex:9: undefined function status/0

Instead, bind the result of the try/rescue block to the value.

status = 
  try do
    raise "boom!"
    "no error"
  rescue
    _ ->
      "rescued"
  end
status
#=> "rescued"

Recap

Putting all of these pieces together looks like this:

try do
  # code that might raise, throw or exit
rescue
  # handle exceptions
catch
  # catch any "thrown" values
  # catch any "exit" signals
after
  # code to always run for cleanup
else
  # code that runs when nothing errors in the try block
end

Within a function body, using a rescue/catch/after will cause the compiler to include a try block for you. This helps the syntax stay a bit cleaner. It looks like this:

def do_work() do
  # code that might raise, throw or exit
rescue
  # handle exceptions
catch
  # catch any "thrown" values
  # catch any "exit" signals
after
  # code to always run for cleanup
end

Some key points to remember:

  • All of these error handling options are not encountered often. They are available but seldom used.
  • The most common one you are likely to use use is raise and rescue. Other libraries may raise exceptions that you must deal with.
  • Pattern matching is preferred to exceptions.
  • Functions that do raise an exception end with a ! bang as a naming convention.

Comments are closed

This is a static version of the site. Comments are not available.

Comments are closed on this static version of the site.