try、catch、rescue

Elixir は3つのエラー機構を持っています。エラー、スロー、イグジットです。この章では、それらがいつ使われるべきかをみていきましょう。

エラー

エラー(または例外)はコード内で予期しない出来事が起きた時に使われます。例えば数値をアトムに加えようとした時に、エラーを発生させることが出来ます。

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

ランタイムエラーは raise/1 を使うことでいつでも発生させることが出来ます。

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

それ以外のエラーは raise/2 にエラー名とキーワード引数のリストを渡すことで発生させることが出来ます。

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

モジュールを作り、その中で defexception/1 マクロを使うことで、独自のエラーを定義することもできます。この方法では、モジュールと同じ名前のエラーを作ることが出来ます。最も一般的な使用法は、独自のメッセージ付きエラーを定義することです。

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

エラーは tru/rescue 構造を用いることで 捕捉 することが出来ます。

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

上の例ではランタイムエラーを補足し、 ies セッション内でエラーが表示されるように、そのエラー自身を返しています。

もしエラーを使う必要がないのであれば、エラー自身を返す必要はありません。

iex> try do
...>   raise "oops"
...> rescue
...>   RuntimeError -> "Error!"
...> end
"Error!"

しかし実際のところ、Elixirで開発中に try/rescue 構造を使うことはほとんどないでしょう。例えば、多くの言語ではファイルを開けなかったときにエラーになるので、あなたがそれを捕捉しなければならないようになっています。一方Elixirが提供する File.read/1 関数は、ファイルを開くことに成功したかどうかの情報を含んだタプルを返します。

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

そこに try/rescue は存在しません。ファイルを開く時の複数の結果を取り扱いたい場合は、 case 構造と一緒にパターンマッチングを使うことが出来ます。

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

要するに、ファイルを開くときのエラーが例外かそうでないかを決めるのはあなたのアプリケーション次第です。だからこそElixirは File.read/1 やその他の関数で例外を強制しません。その代りに、最も良い例外の処理方法を開発者に委ねます。

ファイルが存在することを期待している時(そしてそのファイルが存在しないときは本当にエラーである時)、 File.read!/1 を使うでしょう。

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

標準ライブラリ内の多くの関数は、マッチするタプルを返す代わりに例外を発生させる、という反対のパターンに従います。規約では、 foo 関数を作るときは {:ok, result}{:error, reason} タプルを返し、 foo! という、 foo と同じ名前に ! がつき foo と同じ引数を取る関数は、エラーが生じたときは例外を発生させます。 foo! はすべてが上手く行った時は、タプルで囲わない結果を返すべきです。 File モジュール はこの規約についての良い例です。

Elixirでは、 try/rescue の使用を避けています。なぜなら、 フローを制御するためにエラーを使わないから です。私達はエラーを文字通りに受け取ります。エラーは予期しないまたは予期した例外的状況のために用意されています。フローを制御する構造が必要な場合は、 スロー が使われるべきです。それでは次にスローを見ていきましょう。

スロー

Elixirでは、ある値を投げ、後で捕まえることができます。trhowcatch を使わなければ値を取ることが出来ない状況のために、 throwcatch は予約されています。

それらの状況は、適切なAPIが提供されていないライブラリに直面しなければ、実際にはかなり珍しいことです。例えば、 Enum モジュールが値を探すためのAPIを何一つ用意しておらず、数字のリストから最初の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"

Enum が適切なAPIを 提供している のであれば、実際には Enum.find/2 を使うのがベストです。

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

イグジット

すべてのElixirのコードは、お互いに通信をし合うプロセス群の中で動作します。プロセスが”自然な原因”(未処理の例外など)で死んだ時、プロセスは exit 信号を送ります。プロセスは明示的に exit 信号を送ることで殺すことも出来ます。

iex> spawn_link fn -> exit(1) end
** (EXIT from #PID<0.56.0>) evaluator process exited with reason: 1

上の例ではリンクしていたプロセスが、1という値と一緒に exit 信号が送られ、死にました。Elixirシェルは自動的にこれらのメッセージを処理し、ターミナル上に表示します。

exittry/catch を使うことでも “捕まえる” ことができます。

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

try/catch を使うことは珍しく、それを使ってイグジットを捕まえることはさらに稀です。

exit 信号はErlang VM によって提供される耐障害性の重要な要素です。プロセス群は通常、監視プロセス群からの exit 信号を待ち受けている、監視ツリーの元で動作します。ひとたび exit 信号を受け取ると、監視ストラテジーが実行に移され、監視されているプロセスは再起動されます。

Elixirで try/catchtry/rescue の構造を珍しくするのは、この監視の仕組のおかげです。エラーの後に既知の初期状態にアプリケーションを戻してくれることを監視ツリーが保証してくれるので、エラーを救う代わりに、むしろ “エラー時はただちにシステムを停止” させるのです。

アフター

潜在的にエラーを起こす可能性のあるアクションの後に、リソースが片付けられていることを確実にすることが、時々必要になります。 try/after 構造はそれをできるようにします。例えば、ファイルを開いた時に after 句を使って、失敗したとしてもファイルを閉じることができます。

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

after 句は思考されたブロックが成功したかどうかに関わらず実行されます。しかしリンクされたプロセスが終了した場合は、 このプロセスが終了し after 句が実行されることはない、ということに注意してください。したがって、 after は簡易な保証しか提供しません。運良くElixir内のファイルが現在のプロセス群にリンクされており、そのために現在のプロセスがクラッシュした時に常にファイルが閉じられたとしても、 after 句とは別の話です。ETSテーブルやソケット、ポートなどその他のリソースについても同じことが言えます。

時々、最後にいくつかのコードが実行されることを保証するために、関数内のすべてのコードを try 構造で囲みたいと思うかもしれません。そのような場合は、Elixirでは try 行を省略することができます。

iex> defmodule RunAfter do
...>   def without_even_trying do
...>     raise "oops"
...>   after
...>     IO.puts "cleaning up!"
...>   end
...> end
iex> RunAfter.without_even_trying
cleaning up!
** (RuntimeError) oops

Elixirは afterrescuecatch のいずれかが指定されている時は、自動的に関数の本体を try で囲みます。

エルス

else ブロックが存在する場合は、try ブロックがエラーもスローもなく終了する度に、try ブロックの結果とマッチします。

iex> x = 2
2
iex> try do
...>   1 / x
...> rescue
...>   ArithmeticError ->
...>     :infinity
...> else
...>   y when y < 1 and y > -1 ->
...>     :small
...>   _ ->
...>     :large
...> end
:small

else ブロック内の例外は捕捉されません。else ブロック内のどのパターンともマッチしない場合、例外が起こされます。この例外は現在の try/catch/rescue/after ブロックでは捕捉されません。

変数のスコープ

try/catch/rescue/after ブロックの中で定義された変数は、その外側のコンテキストに漏れることがないということを心に留めておいてください。try ブロックは失敗するかもしれず、ブロック内の変数がそもそも見つかることが無いかもしれないからです。この不正なコードの例で見てみましょう。

iex> try do
...>   raise "fail"
...>   what_happened = :did_not_raise
...> rescue
...>   _ -> what_happened = :rescued
...> end
iex> what_happened
** (RuntimeError) undefined function: what_happened/0

その代りに、try 式の値を保存することが出来ます。

iex> what_happened =
...>   try do
...>     raise "fail"
...>     :did_not_raise
...>   rescue
...>     _ -> :rescued
...>   end
iex> what_happened
:rescued

trycatchrescue についての紹介はこれで終わりです。他の言語と比べてElixirでは、それらがそれほど頻繁に使われないことにあなたは気づくでしょう。しかし、ライブラリや “ルールに従っていない” 一部のコードの中においては、役に立つかもしれません。

Is something wrong? Edit this page on GitHub.