Elixir Errors Handling

Welcome to a tutorial on Error handling in Elixir.

There are three error mechanisms in Elixir; errors, throws, and exits.

 

Error

In Elixir, Errors or Exceptions are used when exceptional things happen in the code. A simple error can be retrieved by trying to add a number into a string, as shown below.

IO.puts(1 + "Hello")

The output is:

** (ArithmeticError) bad argument in arithmetic expression
   :erlang.+(1, "Hello")

The output above is a simple inbuilt error.

 

Raising Errors

Errors can be raised by using the raise function. Check out the example below.

#Runtime Error with just a message
raise "oops"  # ** (RuntimeError) oops

Also, other errors can be raised with raise/2 passing the error name and a list of keyword arguments. This is shown below.

#Other error type with a message
raise ArgumentError, message: "invalid argument foo"

Also, you can define your own errors and raise those. This is shown below.

defmodule MyError do
   defexception message: "default message"
end

raise MyError  # Raises error with default message
raise MyError, message: "custom message"  # Raises error with custom message

 

Rescuing Errors

In Elixir, we don’t want our programs to abruptly quit but rather the errors need to be handled carefully, thus, error handling is used. We can rescue errors by making use of the try/rescue construct. Check out the example below.

err = try do
   raise "oops"
rescue
   e in RuntimeError -> e
end

IO.puts(err.message)

The output is:

oops

You can from the above example, we handle errors in the rescue statement by using pattern matching. However, if we do not have any use for the error, and just want to use it for identification purposes, then we can use the form like this:

err = try do
   1 + "Hello"
rescue
   RuntimeError -> "You've got a runtime error!"
   ArithmeticError -> "You've got a Argument error!"
end

IO.puts(err)

The output is:

You've got a Argument error!

Note that most functions in the Elixir standard library are implemented twice, once returning tuples and the other time raising errors. For instance, the File.read and the File.read! functions. The first one returned a tuple if the file was read successfully, but if an error was encountered, this tuple was used to give the reason for the error, so that the second one raised an error if an error was encountered.

In addition, should in case we use the first function approach, then we need to use the case for pattern matching the error and take action according to that. In the second case, there is need to use the try rescue approach for error-prone code and handle errors accordingly.

 

Throws

A value can be thrown and later be caught, in Elixir. Thus, Throw and Catch are reserved for situations where it is not possible to retrieve a value unless by making use of throw and catch.

The instances are a little uncommon in practice except when interfacing with libraries. Check out the example below, where we assume that the Enum module did not provide any API for finding a value and that we needed to find the first multiple of 13 in a list of numbers.

val = try do
   Enum.each 20..100, fn(x) ->
      if rem(x, 13) == 0, do: throw(x)
   end
   "Got nothing"
catch
   x -> "Got #{x}"
end

IO.puts(val)

The output is: 

Got 26

 

Exit

In the case where a process dies of “natural causes” (e.g. unhandled exceptions), it sends an exit signal. Also, a process can die by explicitly sending an exit signal. Check out the example below.

spawn_link fn -> exit(1) end

From the above example, the linked process died by sending an exit signal with a value of 1. Take note that exit can also be "caught" by using try/catch. Check out the example below.

val = try do
   exit "I am exiting"
catch
   :exit, _ -> "not really"
end

IO.puts(val)

The output is: 

not really

 

After

At times it could be to ensure that a resource is cleaned up after some action that can potentially raise an error. The try/after construct makes it possible to do that. Check out the example below, where we tried to open a file and use an after clause to close it, even if something goes wrong.

{:ok, file} = File.open "sample", [:utf8, :write]
try do
   IO.write file, "olá"
   raise "oops, something went wrong"
after
   File.close(file)
end

If the code above is run, it will give an error. But, with the after a statement, the file descriptor is closed upon any such occurrence.