Elixir - Errors Handling



Elixir has three error mechanisms: errors, throws and exits. Let us explore each mechanism in detail.

Error

Errors (or exceptions) are used when exceptional things happen in the code. A sample error can be retrieved by trying to add a number into a string −

IO.puts(1 + "Hello")

When the above program is run, it produces the following error −

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

This was a sample inbuilt error.

Raising Errors

We can raise errors using the raise functions. Let us consider an example to understand the same −

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

Other errors can be raised with raise/2 passing the error name and a list of keyword arguments

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

You can also define your own errors and raise those. Consider the following example −

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

We do not want our programs to abruptly quit but rather the errors need to be handled carefully. For this we use error handling. We rescue errors using the try/rescue construct. Let us consider the following example to understand the same −

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

IO.puts(err.message)

When the above program is run, it produces the following result −

oops

We have handled errors in the rescue statement using pattern matching. If we do not have any use of the error, and just want to use it for identification purposes, we can also use the form −

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

IO.puts(err)

When running above program, it produces the following result −

You've got a Argument error!

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

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

Throws

In Elixir, a value can be thrown and later be caught. Throw and Catch are reserved for situations where it is not possible to retrieve a value unless by using throw and catch.

The instances are quite uncommon in practice except when interfacing with libraries. For example, let us now 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)

When the above program is run, it produces the following result −

Got 26

Exit

When a process dies of “natural causes” (for example, unhandled exceptions), it sends an exit signal. A process can also die by explicitly sending an exit signal. Let us consider the following example −

spawn_link fn -> exit(1) end

In the example above, the linked process died by sending an exit signal with value of 1. Note that exit can also be “caught” using try/catch. For example −

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

IO.puts(val)

When the above program is run, it produces the following result −

not really

After

Sometimes it is necessary to ensure that a resource is cleaned up after some action that can potentially raise an error. The try/after construct allows you to do that. For example, we can 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

When we run this program, it will give us an error. But the after statement will ensure that the file descriptor is closed upon any such event.

Advertisements