Elixir has three error mechanisms: errors, throws and exits. Let us explore each mechanism in detail.
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.
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
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.
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
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
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.