loading...
gumi TECH Blog

Elixir入門 19: tryとcatchおよびrescue

gumitech profile image gumi TECH ・4 min read

本稿は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/1rescueを加えると、例外名を定めてエラーが回復できます。カスタムエラーを回復して、エラーを取り出すのがつぎの例です。

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で受け取れます。throwcatchを使うのは、ほかに値を受け取る手段がない場合です。

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"

exittry/catchを用いて捉えることもできます。

iex> try do
...>   exit "oh no!"
...> catch
...>   :exit, _ -> "exit blocked"
...> end
"exit blocked"
iex>

ただし、try/catchを使うことは少なく、さらにそれでexitを捉えることはほとんどありません。

exit信号は、Erlang VMが提供する耐障害システムにおいて、重要な役割を果たします。プロセスは、スーパーバイザーの監視ツリーのもとで動くのが通常です。監視ツリーもプロセスで、監視プロセスからのexit信号を検知します。exit信号を受け取ると、監視システムが働いて監視プロセスを再起動します。

監視システムがまさにtry/catchtry/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ブロックは省けます。関数の中でafterrescueあるいは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入門もくじ

番外

Posted on by:

gumitech profile

gumi TECH

@gumitech

gumi TECH は、株式会社gumiのエンジニアによる技術記事公開やDrinkupイベントなどの技術者交流を行うアカウントです。 gumi TECH Blog: http://dev.to/gumi / gumi TECH Drinkup: http://gumitech.connpass.com

gumi TECH Blog

株式会社gumiのエンジニアによる技術記事を公開しています。

Discussion

pic
Editor guide