Elixir入門 19: tryとcatchおよびrescue
Tags: elixir, webdev, tutorial, programming
Published: 2019-01-29 01:10:32

---
title: Elixir入門 19: tryとcatchおよびrescue
published: true
description: Elixir公式サイトの許諾を得て「try, catch and rescue」の解説にもとづき、加筆補正を加えて、Elixirにおけるエラーの扱いについてご説明します。
tags: #elixir, #webdev, #tutorial, #programming
---

本稿はElixir公式サイトの許諾を得て「[try, catch and rescue](https://elixir-lang.org/getting-started/try-catch-and-rescue.html)」の解説にもとづき、加筆補正を加えて、Elixirにおけるエラーの扱いについてご説明します。

Elixirが備えるエラーの仕組みは3つあります。エラーとスロー(`throw`)および終了(`exit`)です。それぞれの内容と使い方について解説しましょう。


# エラー
エラーあるいは例外は、コードに例外的なことが起こったときに使われます。たとえば、アトムに数値を加えたときです。計算式の引数が正しくないというエラーになります。

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

実行時のエラーは[`raise/1`](https://hexdocs.pm/elixir/Kernel.html#raise/1)で起こせます。引数はエラーとともにメッセージ示されるメッセージです。

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

エラーを起こす関数には[`raise/2`](https://hexdocs.pm/elixir/Kernel.html#raise/2)もあります。第1引数がエラーモジュール名で、第2引数はプロパティのキーワードリストです。

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


独自のエラーも定められます。モジュールに[`defexception/1`](https://hexdocs.pm/elixir/Kernel.html#defexception/1)で、エラーとして返す構造体をつくるのです。すると、モジュール名のエラーがつくられます。例外の構造体に与えるフィールドとしてよく使われるのは`message`です。

```elixir
defmodule MyError do
  defexception message: "default message"
end
```

```elixir
iex> raise MyError
** (MyError) default message
iex> raise MyError, message: "custom message"
** (MyError) custom message
```

[`try/1`](https://hexdocs.pm/elixir/Kernel.SpecialForms.html#try/1)に[`rescue`](https://hexdocs.pm/elixir/Kernel.SpecialForms.html#try/1-rescue-clauses)を加えると、例外名を定めてエラーが回復できます。カスタムエラーを回復して、エラーを取り出すのがつぎの例です。

```elixir
iex> try do
...>   raise MyError
...> rescue
...>   err in MyError -> err
...> end
%MyError{message: "default message"}
```

つぎの例は、実行時エラーが起こったとき、エラーを取り出します。

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

エラーをとくに使わない場合は、取り出さなくても構いません。

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

実際には、Elixir開発者が`try`/`rescue`構文を使うことはほとんどありません。他の多くの言語では、たとえばファイルが正しく開けなかったときは、エラーを回復しなければならないでしょう。けれども、Elixirの[`File.read/1`関数](https://hexdocs.pm/elixir/File.html#read/1)は、ファイルが正しく開けたかどうかの情報をタプルで返すだけです。

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

`try`/`rescue`は使わなくて構いません。結果によって扱いを変えたいときは、`case`文でパターンマッチングを用いればよいのです。

```elixir
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
```

```elixir
iex> Example.read_file("hello")
Success: world
:ok
iex> Example.read_file("unknown")
Error: enoent
:ok
```

ファイルの読み込みについて、ファイルは存在する前提で、ないときにエラーが起こるようにしたい場合には[`File.read!/1`](https://hexdocs.pm/elixir/File.html#read!/1)をお使いください。

```elixir
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](https://hexdocs.pm/elixir/naming-conventions.html))です。戻り値はタプルでなく直接の結果とし、エラーの場合には例外を起こします(「[Trailing bang (foo!)](https://hexdocs.pm/elixir/naming-conventions.html#trailing-bang-foo)」参照)。

Elixirでは`try`/`rescue`は使わないようにしています。制御フローにエラーを用いないからです。エラーというのは文字どおり、予期しない例外的な状況を意味するのです。実際にフローを制御する仕組みが求められるときは、`throw`をお使いください。


# throw
Elixirでは、値を[`throw/1`](https://hexdocs.pm/elixir/Kernel.html#throw/1)でスローし、あとから[`catch`](https://hexdocs.pm/elixir/Kernel.SpecialForms.html#try/1-catch-clauses)で受け取れます。`throw`と`catch`を使うのは、ほかに値を受け取る手段がない場合です。

```elixir
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`](https://hexdocs.pm/elixir/Enum.html#find/3)が使えます。

```elixir
iex> Enum.find(0..10, &(&1 >  4))
5
```


# exit
Elixirのコードはすべてプロセスの中で動き、プロセスは互いに通信します。プロセスが「自然な原因」(たとえば処理できない例外)で終了すると、`exit`の信号が送られます。プロセスはまた、[`exit/1`](https://hexdocs.pm/elixir/Kernel.html#exit/1)で信号を明示的に発信することにより終了することもできるのです。すると、Elixirシェルは自動的にメッセージを端末に出力します。

```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`を用いて捉えることもできます。

```elixir
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`構文](https://hexdocs.pm/elixir/Kernel.SpecialForms.html#try/1-after-clauses)です。

```elixir
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`で閉じれば、開いたファイルに何か問題が起こっても必ず閉じられます。

```elixir
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
```

```elixir
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`でラップするからです。

```elixir
defmodule RunAfter do
  def without_even_trying do
    raise "oops"
  after
    IO.puts "cleaning up!"
  end
end
```

```elixir
iex> RunAfter.without_even_trying
cleaning up!
** (RuntimeError) oops
```

# else
`try`構文に[`else`句](https://hexdocs.pm/elixir/master/Kernel.SpecialForms.html#try/1-else-clauses)を加えると、`try`ブロックがスローまたはエラーなしに処理されたとき、その結果が`else`ブロックにマッチします。

```elixir
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`ブロックは失敗するかもしれないので、変数を外部にバインドさせないのです。

```elixir
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`構文の結果を外から参照したいときは、式の値を変数に入れればよいのです。

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


#### Elixir入門もくじ
- [Elixir入門 01: コードを書いて試してみる](https://dev.to/gumi/elixir-01--2585)
- [Elixir入門 02: 型の基本](https://dev.to/gumi/elixir-02--30n1)
- [Elixir入門 03: 演算子の基本](https://dev.to/gumi/elixir-03--33im)
- [Elixir入門 04: パターンマッチング](https://dev.to/gumi/elixir-04--1346)
- [Elixir入門 05: 条件 - case/cond/if](https://dev.to/gumi/elixir-05----casecondif-60o)
- [Elixir入門 06: バイナリと文字列および文字リスト](https://dev.to/gumi/elixir-06--35na)
- [Elixir入門 07: キーワードリストとマップ](https://dev.to/gumi/elixir-07--39hi)
- [Elixir入門 08: モジュールと関数](https://dev.to/gumi/elixir-08--1c4c)
- [Elixir入門 09: 再帰](https://dev.to/gumi/elixir-09--1a0p)
- [Elixir入門 10: EnumとStream](https://dev.to/gumi/elixir-10-enumstream-4fpb)
- [Elixir入門 11: プロセス](https://dev.to/gumi/elixir-11--2mia)
- [Elixir入門 12: 入出力とファイルシステム](https://dev.to/gumi/elixir-12--4og6)
- [Elixir入門 13: aliasとrequireおよびimport](https://dev.to/gumi/elixir-13-aliasrequireimport-55c1)
- [Elixir入門 14: モジュールの属性](https://dev.to/gumi/elixir-14--3511)
- [Elixir入門 15: 構造体](https://dev.to/gumi/elixir-15--4f43)
- [Elixir入門 16: プロトコル](https://dev.to/gumi/elixir-16--lif)
- [Elixir入門 17: 内包表記](https://dev.to/gumi/elixir-17--5gci)
- [Elixir入門 18: シギル](https://dev.to/gumi/elixir-18--5791)
- Elixir入門 19: tryとcatchおよびrescue
- [Elixir入門 20: 型の仕様とビヘイビア](https://dev.to/gumi/elixir-20--j50)
- [Elixir入門 21: デバッグ](https://dev.to/gumi/elixir-21--21a1)
- [Elixir入門 22: Erlangライブラリ](https://dev.to/gumi/elixir-22-erlang-2492)
- [Elixir入門 23: つぎのステップ](https://dev.to/gumi/elixir-23--50ik)

##### 番外
- [Elixir入門: Plugを使うには](https://dev.to/gumi/elixir-plug-40lb)