Monday, January 30, 2017

Elixir - Error Handling

Elixir has three error mechanisms: errors, throws and exits. Let us explore each of them.

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 an string:
IO.puts(1 + "Hello")
When running above program, it produces 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 see an example:
#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. For 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 don't want our programs to abruptly quit but rather handle these errors gracefully. For this we use error handling. We rescue errors using the try/rescue construct. Let us have a look at an example:
err = try do
    raise "oops"
rescue
    e in RuntimeError -> e
end

IO.puts(err.message)
When running above program, it produces following result:
oops
What we have done here is handle errors in the rescue statement using pattern matching. If we dont 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 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 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. It is entirely upto you.

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.
Those situations are quite uncommon in practice except when interfacing with libraries that do not provide a proper API. For example, let’s imagine 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 running above program, it produces following result:
Got 26

Exit

When a process dies of “natural causes” (e.g., unhandled exceptions), it sends an exit signal. A process can also die by explicitly sending an exit signal. For 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 running above program, it produces following result:
not really

After

Sometimes it’s necessary to ensure that a resource is cleaned up after some action that could 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'll give us an error. But the after statement will ensure that the file descriptor is closed upon any such event.

No comments:

Post a Comment