Elixir入門 19: tryとcatchおよびrescue
gumi TECH
Posted on January 29, 2019
本稿はElixir公式サイトの許諾を得て「try, catch and rescue」の解説にもとづき、加筆補正を加えて、Elixirにおけるエラーの扱いについてご説明します。
Elixirが備えるエラーの仕組みは3つあります。エラーとスロー(throw
)および終了(exit
)です。それぞれの内容と使い方について解説しましょう。
エラー
エラーあるいは例外は、コードに例外的なことが起こったときに使われます。たとえば、アトムに数値を加えたときです。計算式の引数が正しくないというエラーになります。
iex> :atom + 1
** (ArithmeticError) bad argument in arithmetic expression
:erlang.+(:atom, 1)
実行時のエラーはraise/1
で起こせます。引数はエラーとともにメッセージ示されるメッセージです。
iex> raise "oops"
** (RuntimeError) oops
エラーを起こす関数にはraise/2
もあります。第1引数がエラーモジュール名で、第2引数はプロパティのキーワードリストです。
iex> raise(ArgumentError, message: "invalid argument")
** (ArgumentError) invalid argument
独自のエラーも定められます。モジュールにdefexception/1
で、エラーとして返す構造体をつくるのです。すると、モジュール名のエラーがつくられます。例外の構造体に与えるフィールドとしてよく使われるのはmessage
です。
defmodule MyError do
defexception message: "default message"
end
iex> raise MyError
** (MyError) default message
iex> raise MyError, message: "custom message"
** (MyError) custom message
try/1
にrescue
を加えると、例外名を定めてエラーが回復できます。カスタムエラーを回復して、エラーを取り出すのがつぎの例です。
iex> try do
...> raise MyError
...> rescue
...> err in MyError -> err
...> end
%MyError{message: "default message"}
つぎの例は、実行時エラーが起こったとき、エラーを取り出します。
iex> try do
...> raise "oops"
...> rescue
...> err in RuntimeError -> err
...> end
%RuntimeError{message: "oops"}
エラーをとくに使わない場合は、取り出さなくても構いません。
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
文でパターンマッチングを用いればよいのです。
defmodule Example do
def read_file(file) do
case File.read file do
{:ok, body} -> IO.puts "Success: #{body}"
{:error, reason} -> IO.puts "Error: #{reason}"
end
end
end
iex> Example.read_file("hello")
Success: world
:ok
iex> Example.read_file("unknown")
Error: enoent
:ok
ファイルの読み込みについて、ファイルは存在する前提で、ないときにエラーが起こるようにしたい場合にはFile.read!/1
をお使いください。
iex> File.read! "unknown"
** (File.Error) could not read file "unknown": no such file
or directory
(elixir) lib/file.ex:310: File.read!/1
標準ライブラリの多くの関数には、結果を{:ok, result}
や{:error, reason}
といったタプルで返すものと、エラーで例外が起こるものの2種類あり、後者には決まった名前のつけ方をします。タプルを返す関数に対して、同じアリティで同じ名前のあとに!
をつけるのが命名規則(Naming Conventions)です。戻り値はタプルでなく直接の結果とし、エラーの場合には例外を起こします(「Trailing bang (foo!)」参照)。
Elixirではtry
/rescue
は使わないようにしています。制御フローにエラーを用いないからです。エラーというのは文字どおり、予期しない例外的な状況を意味するのです。実際にフローを制御する仕組みが求められるときは、throw
をお使いください。
throw
Elixirでは、値をthrow/1
でスローし、あとからcatch
で受け取れます。throw
とcatch
を使うのは、ほかに値を受け取る手段がない場合です。
iex> try do
...> for x <- 0..10 do
...> if x > 4, do: throw(x)
...> IO.puts(x)
...> end
...> catch
...> x -> "Caught: #{x}"
...> end
0
1
2
3
4
"Caught: 5"
上の例では、Enum
モジュールが適切なAPIを備えているので、実際にはEnum.find/3
が使えます。
iex> Enum.find(0..10, &(&1 > 4))
5
exit
Elixirのコードはすべてプロセスの中で動き、プロセスは互いに通信します。プロセスが「自然な原因」(たとえば処理できない例外)で終了すると、exit
の信号が送られます。プロセスはまた、exit/1
で信号を明示的に発信することにより終了することもできるのです。すると、Elixirシェルは自動的にメッセージを端末に出力します。
iex> spawn_link fn -> exit("oh no") end
** (EXIT from #PID<0.84.0>) shell process exited with reason: "oh no"
exit
はtry
/catch
を用いて捉えることもできます。
iex> try do
...> exit "oh no!"
...> catch
...> :exit, _ -> "exit blocked"
...> end
"exit blocked"
iex>
ただし、try
/catch
を使うことは少なく、さらにそれでexit
を捉えることはほとんどありません。
exit
信号は、Erlang VMが提供する耐障害システムにおいて、重要な役割を果たします。プロセスは、スーパーバイザーの監視ツリーのもとで動くのが通常です。監視ツリーもプロセスで、監視プロセスからのexit
信号を検知します。exit
信号を受け取ると、監視システムが働いて監視プロセスを再起動します。
監視システムがまさにtry
/catch
やtry
/rescue
といった仕組みをつくるので、Elixirで使うことは少ないのです。したがって、エラーを回復するのではなく、むしろ速やかに失敗させます。そうすれば、監視ツリーがエラーのあと、アプリケーションを必ず初期の状態に戻してくれるのです。
after
エラーが起こる可能性のある処理のあと、必ずリソースを解放しなければならない場合もあります。これを扱うのがtry
/after
構文です。
iex> try do
...> raise "Oh no!"
...> rescue
...> err in RuntimeError -> IO.puts("An error occurred: " <> err.message)
...> after
...> IO.puts "The end!"
...> end
An error occurred: Oh no!
The end!
:ok
たとえば、ファイルを開いたあとafter
で閉じれば、開いたファイルに何か問題が起こっても必ず閉じられます。
defmodule Example do
def handle_file(path, string) do
{:ok, file} = File.open(path, [:utf8, :write])
try do
IO.write file, string
raise "oops, something went wrong"
after
File.close(file)
IO.puts "the file is closed"
end
end
end
iex> Example.handle_file("hello", "world")
the file is closed
** (RuntimeError) oops, something went wrong
after
節はtry
ブロックが成功したかどうかにかかわらず実行されます。ただし、リンクされたプロセスが終了すると、現行のプロセスも終了し、after
節は処理されません。もっとも、Elixirにおいてファイルは現行のプロセスにリンクしています。したがって、そのプロセスがクラッシュすれば、after
節にかかわりなく、つねに閉じられます。ETSやテーブル、ソケット、ポートなど他のリソースについても同様です。
場合によっては、関数本体すべてをtry
構文に入れて、後処理が必ず行われるようにafter
で定めたいこともあるでしょう。そうしたとき、try
ブロックは省けます。関数の中でafter
やrescue
あるいはcatch
が使われると、Elixirが自動的に関数本体をtry
でラップするからです。
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
else
try
構文にelse
句を加えると、try
ブロックがスローまたはエラーなしに処理されたとき、その結果がelse
ブロックにマッチします。
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> what_happened = :outside
:outside
iex> try do
...> raise "fail"
...> what_happened = :did_not_raise
...> rescue
...> _ -> what_happened = :rescued
...> end
:rescued
iex> what_happened
:outside
try
構文の結果を外から参照したいときは、式の値を変数に入れればよいのです。
iex> what_happened =
...> try do
...> raise "fail"
...> :did_not_raise
...> rescue
...> _ -> :rescued
...> end
:rescued
iex> what_happened
:rescued
Elixir入門もくじ
- Elixir入門 01: コードを書いて試してみる
- Elixir入門 02: 型の基本
- Elixir入門 03: 演算子の基本
- Elixir入門 04: パターンマッチング
- Elixir入門 05: 条件 - case/cond/if
- Elixir入門 06: バイナリと文字列および文字リスト
- Elixir入門 07: キーワードリストとマップ
- Elixir入門 08: モジュールと関数
- Elixir入門 09: 再帰
- Elixir入門 10: EnumとStream
- Elixir入門 11: プロセス
- Elixir入門 12: 入出力とファイルシステム
- Elixir入門 13: aliasとrequireおよびimport
- Elixir入門 14: モジュールの属性
- Elixir入門 15: 構造体
- Elixir入門 16: プロトコル
- Elixir入門 17: 内包表記
- Elixir入門 18: シギル
- Elixir入門 19: tryとcatchおよびrescue
- Elixir入門 20: 型の仕様とビヘイビア
- Elixir入門 21: デバッグ
- Elixir入門 22: Erlangライブラリ
- Elixir入門 23: つぎのステップ
番外
Posted on January 29, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.