17 トライ,キャッチとレスキュー - try, catch and rescue

エリクサーは3つのエラー機構を持っています: エラー,スローそしてイグジットです.この章ではそれらについて,それぞれどんな時に使われるとよいかも含めて詳しくみていきましょう.

Elixir has three error mechanisms: errors, throws and exits. In this chapter we will explore each of them and include remarks about when each should be used.

17.1 エラー - Errors


A sample error can be retrieved by trying to add a number into an atom:

iex> :foo + 1
** (ArithmeticError) bad argument in arithmetic expression
     :erlang.+(:foo, 1)


A runtime error can be raised any time by using the raise/1 macro:

iex> raise "oops"
** (RuntimeError) oops


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

iex> raise ArgumentError, message: "invalid argument foo"
** (ArgumentError) invalid argument foo


You can also define your own errors by creating a module and use the defexception/1 macro inside it. The most common case is to define an exception with a message field:

iex> defmodule MyError do
iex>  defexception message: "default message"
iex> end
iex> raise MyError
** (MyError) default message
iex> raise MyError, message: "custom message"
** (MyError) custom message


Exceptions can be rescued by using the try/rescue construct:

iex> try do
...>   raise "oops"
...> rescue
...>   e in RuntimeError -> e
...> end
%RuntimeError{message: "oops"}


The example above rescues the runtime error and returns the error itself which is then printed in the iex session. In practice Elixir developers rarely use the try/rescue construct though. For example, many languages would force you to rescue an error when a file cannot open successfully. Elixir instead provides a File.read/1 function which returns a tuple containing information if the file was opened with success or not:

iex> File.read "hello"
{:error, :enoent}
iex> File.write "hello", "world"
iex> File.read "hello"
{:ok, "world"}


There is no try/rescue here. In case you want to handle multiple outcomes of opening a file, you can simply use pattern matching with case:

iex> case File.read "hello" do
...>   {:ok, body} -> IO.puts "got ok"
...>   {:error, body} -> IO.puts "got error"
...> end


At the end of the day, it is up to your application to decide if an error while opening a file is exceptional or not. That's why Elixir doesn't impose exceptions on File.read/1 and many other functions. Instead we leave it up to the developer to choose the best way to proceed.


For the cases where you do expect a file to exist (and the lack of a file is truly an error) you can simply use File.read!/1:

iex> File.read! "unknown"
** (File.Error) could not read file unknown: no such file or directory
    (elixir) lib/file.ex:305: File.read!/1


In other words, we avoid using try/rescue because we don't use errors for control flow. In Elixir, we take errors literally: they are reserved to unexpected and/or exceptional situations. In case you actually need flow control constructs, throws must be used. That's what we are going to see next.

17.2 スロー - Throws


In Elixir, one can throw a value to be caught later. 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 unless when interfacing with a library that does not provide the proper APIs. For example, let's imagine the Enum module did not provide any API for finding a value and we need to find the first number that is a multiple of 13:

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


However, in practice one can simply use Enum.find/2:

iex> Enum.find -50..50, &(rem(&1, 13) == 0)

17.3 イグジット - Exits


Every Elixir code runs inside processes that communicates with each other. When a process dies, it sends an exit signal. A process can also die by explicitly sending an exit signal:

iex> spawn_link fn -> exit(1) end
** (EXIT from #PID<0.56.0>) 1


In the example above, the linked process died by sending an exit signal with value of 1. The Elixir shell automatically handles those messages and prints them to the terminal.


exit can also be "caught" using try/catch:

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


Using try/catch is already uncommon and using it to catch exits is even more rare.

exitシグナルはErlang VMで提供されている耐障害性において重要な部分を担っています.プロセスは通常,単にプロセスのexit信号を待つだけの監視プロセスを備えた監視ツリーにくみこまれて動作しています.監視プロセスがイグジット信号を受けとると,監視作戦が開始され,監視対象のプロセスは再起動させられます.

exit signals are an important part of the fault tolerant system provided by the Erlang VM. Processes usually run under supervision trees which are themselves processes that just wait for exit signals of the supervised processes. Once an exit signal is received, the supervision strategy kicks in and the supervised process is restarted.


It is exactly this supervision system that makes constructs like try/catch and try/rescue so uncommon in Elixir. Instead of rescuing a certain error, we'd rather "fail fast" since the supervision tree will guarantee our application will go back to a known initial state after the error.

17.4 アフター - After


Sometimes it is necessary to use try/after to guarantee a resource is cleaned up after some particular action. For example, we can open a file and guarantee it is closed with try/after block:

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

17.5 変数のスコープ - Variables scope


It is important to bear in mind that variables defined inside try/catch/rescue/after blocks do not leak to the outer context. This is because the try block may fail and as such the variables may never be bound in the first place. In other words, this code is invalid:

iex> try do
...>   from_try = true
...> after
...>   from_after = true
...> end
iex> from_try
** (RuntimeError) undefined function: from_try/0
iex> from_after
** (RuntimeError) undefined function: from_after/0


This finishes our introduction to try, catch and rescue. You will find they are used less frequently in Elixir than in other languages although they may be handy in some situations where a library or some particular code is not playing "by the rules".


It is time to talk about some Elixir constructs like comprehensions and sigils.