Swiftのエラー4分類が素晴らしすぎるのでみんなに知ってほしい

これは [Swift Tweets](https://swift-tweets.github.io/) の発表をまとめたものです（[次回開催はこちら](https://swift-tweets.connpass.com/event/48887/)）。イベントのスポンサーとして Qiita に許可をいただいた上で投稿しています。

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">ありがとうございました！Q&amp;Aは他の人の発表中でも構わないのでリプを飛ばして下さい。<br><br>続いては僕 <a href="https://twitter.com/koher">@koher</a> の発表で、タイトルは &quot;Swiftのエラー4分類が素晴らしすぎるのでみんなに知ってほしい&quot; です。 <a href="https://twitter.com/hashtag/swtws?src=hash">#swtws</a></p>&mdash; koher (@koher) <a href="https://twitter.com/koher/status/820264215877324801">2017年1月14日</a></blockquote>

# 第 1 部: Swift の 4 種類のエラーについて

あまり知られてませんが、エラー処理について、 Swift 2.0 設計時に Core Team がまとめた ["Error Handling Rationale and Proposal"](https://github.com/apple/swift/blob/main/docs/ErrorHandlingRationale.md) というドキュメントがあります。このドキュメントは、僕が[去年 try! Swift で発表](https://github.com/koher/try-swift-2016/blob/master/slides.md)した際にも参考文献にしました。長いし（僕にとっては）英語が難しいし、具体例も少ないしで読むのがとても大変でした。

その中でエラーの分類について記述があるんですが、当時ピンと来なかったのが 1 年かけて逆に素晴らしいと思えてきたので、今日はそれについて発表します。本質的で重要なことだと思うので、もし1年前に戻れるなら try! Swift の発表テーマをこれに変更したいくらいです。

## エラーの種類

それによると、エラー発生時プログラマに『どのように対応させたいか』によって、エラーは

- _**Simple domain error**_
- _**Recoverable error**_
- _**Universal error**_
- _**Logic failure**_

の4種類に分類されます。

『どのように対応させたいか』というところがキモです。 **エラーの分類というと、エラーの内容によって分類してしまいそうですが、そうではなくエラーに『どのように対応させたいか』です。** どういうことでしょうか。

### 例

例として、文字列を整数に変換する関数 `toInt` を考えてみます。この関数は、 `"123"` のような文字列であれば整数に変換可能ですが、もし `"xyz"` のような文字列が渡された場合は整数に変換することができず、エラーとなります。

#### Simple domain error

文字列→整数の変換に失敗するなんて原因は明白なんだから、エラーが起こったかどうかさえわかればいいんだ、と考える場合に採用します。エラーが発生したことだけを伝え、その原因についての情報は伝えません。

Swift なら、 `Optional` を使って戻り値の型を `Int?` とし、 `nil` を返すことで _Simple domain error_ を表現できます。

```swift
func toInt(_ string: String) -> Int? { ... }

guard let number = toInt(string) else {
  print("整数を入力して下さい。")
  return
}

// `number` を使う処理
```

#### Recoverable error

たとえ文字列→整数の変換であっても、原因によって処理の方法が異なるだろう、と考える場合に採用します。たとえば、文字列が整数だったけど `Int` の範囲で表せない（オーバーフローする）場合とその他の場合を区別したいかもしれません。

Swift なら、 `Error` 型で _Recoverable error_ を表現できます。 `toInt` には `throws` を付与し、 `do` - `try` - `catch` を使ってエラー処理をします。

```swift
func toInt(_ string: String) throws -> String { ... }

do {
  let number = try toInt(string)
  // `number` を使う処理
} catch ConversionError.overflow {
  print("\(Int.min)〜\(Int.max)の値を入力して下さい。")
  return
} catch {
  print("整数を入力して下さい。")
  return
}
```

Recoverable （回復可能）というのは、残りの _Universal error_ と _Logic failure_ が回復可能 **でない** こととの対比です。 _Simple domain error_ も回復可能なので _Recoverable error_ という名前は微妙な気もします。

#### Universal error

整数に変換できないような不正な文字列が `toInt` に渡されたらプログラムが停止すればいい、エラー処理をする必要はないと、考える場合に採用します。

Swift では、 _Universal error_ を発生させるために `fatalError` を使います。 Swift 3 時点では、 `fatalError` を処理して回復することはできません。

```swift
func toInt(_ string: String) -> Int {
  guard isPossibleToConvert(string) else {
    fatalError()
  }
  ...
}

let number = toInt(string) // 変換に失敗したらクラッシュ
// `number` を使う処理
```

#### Logic failure

整数に変換できない文字列を `toInt` に渡していること自体がバグだ、コードを修正する必要がある、と考える場合に採用します。 _Logic failure_ は実行時に起こってはいけないので、起こった場合の挙動は未定義です。

Swift では、 _Logic failure_ を起こさないために満たすべき前提条件を `precondition` で表します。 `precondition` は `-Ounchecked` でビルドすると無視され、実行時のオーバーヘッドをなくすことができます。

```swift
func toInt(_ string: String) -> Int {
  precondition(isPossibleToConvert(string))
  ...
}

let number = toInt(string) // 変換に失敗したら未定義動作
// `number` を使う処理
```

### 『どのようにエラーに対応させたいか』によってエラーを分類する

このように、文字列→整数の変換エラーという内容ではなく、『どのようにエラーに対応させたいか』によってエラーを分類します。どうしてエラーの内容ではなく『どのようにエラーに対応させたいか』によってエラーを分類するのでしょうか。

当たり前ですが、僕らが関数やメソッドを設計するときには、利用者に『どのようにエラーに対応させたいか』を考えなければなりません。 `nil` を返すのか、 `Error` を `throw` するのか、それともクラッシュさせるのか、必ず判断をしているはずです。そう考えると、『どのようにエラーに対応させたいか』によるエラー分類は特別なことではなく、関数・メソッドの設計時には誰もが当たり前にやっていることのはずです。

## エラーの使い分け

しかし、僕らは一貫した適切なポリシーを持ってエラーを分類できているでしょうか。もしそうでなければ、アプリやライブラリ全体としてポリシーのブレたわかりづらい設計になってしまっているはずです。

### 例

"Error Handling Rationale and Proposal" には、4種類のエラーの使い分けの例も書かれています。それを紹介しましょう。

#### Simple domain error の例

整数→文字列の変換エラーが挙げられています。発生の前提条件が明確なので、エラーが起こったという結果さえわかれば良いという考えです。

```swift:simple-domain-error.swift
guard let number = Int(string) else {
  print("整数を入力して下さい。")
  return
}

// `number` を使う処理
```

#### Recoverable error の例

ファイル入出力やネットワークのエラーが挙げられています。発生条件が明確でなく、どのように回復すべきか原因によって対応方法が異なるので、その手段を提供するべきという考えです。

```swift:recoverable-error.swift
// ※説明のために架空の `Data` の `init` を使っています。
do {
  let data = try Data(at: path)
  // `data` を使う処理
} catch FileError.noSuchFile {
  // ファイルがない場合のエラー処理
} catch FileError.permissionDenied {
  // パーミッションがない場合のエラー処理
} catch {
  // その他の場合のエラー処理
}
```

#### Universal error の例

メモリ不足やスタックオーバーフローなどのエラーが挙げられています。利用者側で対応しようがないのでプログラムを停止すべきという考えです。

```swift:universal-error.swift
func reduce<T, R>(_ range: CountableRange<T>, _ initial: R, _ combine: (R, T) -> R) -> R {
  guard let first = range.first else { return initial }
  return reduce(
    range.startIndex.advanced(by: 1) ..< range.endIndex,
    combine(initial, first),
    combine
  )
}

reduce(0..<1000000, 0) { $0 + $1 } // スタックオーバーフローでクラッシュ
```

#### Logic failure の例

`Array` のインデックスがはみ出てしまった場合や、 `nil` が入っている `Optional` に対して  _Forced unwrapping_ を実行してしまった場合などが挙げられています。

```swift:logic-failure.swift
let array = [2, 3, 5]
print(array[3])
```

### 使い分けの考え方

注意してほしいのは、あくまでこの例は「多くの場合」に適しているだけで、絶対的な指針ではありません。繰り返しになりますが、同じエラーでも『どのようにエラーに対応させたいか』次第で、 4 種類のどれを採用することもあり得ます。

たとえば、 _Universal error_ の例としてメモリ不足が挙げられていますが、巨大なメモリ領域を確保するようなケースでメモリが足りなければエラー処理をしたいでしょう。そのようなケースでは _Simple domain error_ が適切です。

僕が一番おもしろいと思い、感銘を受けたのが _Logic failure_ です。どうして `Array` はインデックスが範囲外だった場合に _Simple domain error_ としてnilを返さないのでしょうか。 `Dictionary` はキーに対応した値がなければ `nil` を返します。

僕が初めて Swift に触れたとき、これはパフォーマンス上の理由だと考えていました。インデックスが不正でないかチェックしようとするとその度にオーバーヘッドが発生します。画像処理などで繰り返し `subscript` が呼ばれるときに、そのオーバーヘッドは無視できません。

もちろんそういう理由もあるでしょう。しかしよく考えてみると、 `Array` の要素に `subscript` でアクセスするのにインデックスがはみ出してしまうようなケースは、ほとんどがコーディング時のミスが原因だとわかります。

試しに、インデックスが範囲外だった場合にエラー処理をして回復する必要がある具体的な処理を何か思い浮かべてみて下さい。僕が思い付いたのは[番兵]( https://ja.wikipedia.org/wiki/%E7%95%AA%E5%85%B5)の代わりや畳み込み等の画像処理くらいです。コードに問題があるのであれば回復『不能』であり、実行時にエラーに対処することはできません。コードのバグを修正すべきであり、 `Array` の `subscript` のエラーを _Logic failure_ として扱うのは理にかなっています。

ところで、 `Array` のインデックスがはみ出たときはクラッシュ、つまり _Universal error_ なんじゃないかと思うかもしれません。次のコードを実行してみて下さい。

```swift:out-of-bounds.swift
let array = [2, 3, 5]
print(array[3])
```

クラッシュしましたね。では、ファイル名を out-of-bounds.swift として、次のように `-Ounchecked` で実行してみて下さい。実行する度に結果が変わる（未定義動作な）のが確認できるはずです。

```bash:run-out-of-bounds.sh
swift -Ounchecked out-of-bounds.swift
```

`Array` のインデックスが範囲外になるのはコードの問題（ _Logic failure_ ）なのだから、実行時に範囲チェックする必要はないわけです。その上で、開発時（ `-Ounchecked` でないとき）は素早くコードの誤りに気付けるようにチェックして停止させてくれるわけです。

## まとめ

- 『どのように対応させたいか』でエラー分類
- 前提条件が明確→ _Simple domain error_
- 原因ごとに対応→ _Recoverable error_
- 回復不能→ _Universal error_
- コードの誤り→ _Logic failure_

"Error Handling Rationale and Proposal"のエラー分類はよく考えれば当たり前のことです。しかし、改めて考え直すと、自分が設計した関数やメソッドでこれを徹底できていたと自信を持って言える人は少ないのではないでしょうか。

# 第 2 部: Java との比較からエラー分類の重要性を探る

ところで、僕が大学生だった頃、ヒューマンインターフェース工学の授業で人間の特性 8 箇条というものを習いました。「人間は気まぐれである」、「人間はなまけものである」、「人間は不注意である」、「人間は根気がない」、「人間は単調をきらう」などです。これは、何かを設計するときには人間はそのような欠点を持っているものと考えて、それでも問題が起こらないようにすべきだという指針です。

## C 言語とエラー処理

しかし、当時僕が使っていた C 言語はとてもそのように設計されているとは思えませんでした。

たとえば、 C 言語ではファイルを開くには次のようなコードを書きます。もし「不注意」でエラー処理を忘れてしまったら、 `file` を使おうとしてクラッシュするまで気付くことができません。

```c
FILE *file = fopen("filename", "r");
if (file == NULL) { // ファイルを開くのに失敗したとき
  // エラー処理
}
// `file` を使う処理
```

人間は「気まぐれ」でエラー処理を書いたり書かなかったりします。異常系は起こらないことが多いので「なまけ」て省略してしまいがちです。省略しないと決めても「不注意」で簡単に忘れてしまいますし「根気がない」ので長続きしません。そして、エラー処理の大半は「単調な」作業です。

##  Java と検査例外と Swift

初めてJavaに触れて検査例外を知ったとき、素晴らしい仕組みだと思いました。エラー処理を忘れたらコンパイラが教えてくれます。ヒューマンエラーを防ぐ画期的な仕組みです。

```java
try { // try-catch を書かないとコンパイルエラー
  RandomAccessFile file = new RandomAccessFile(new File("filename"), "r");
  // `file` を使う処理
} catch (FileNotFoundException e) { // ファイルを開くのに失敗したとき
  // エラー処理
}
```

しかし、 Java の検査例外は失敗とみなされ、 C# や Kotlin などの後続言語には取り入れられませんでした。ヒューマンエラーを防ぐ素晴らしい仕組みだったのになぜでしょうか。

Java の検査例外が良くないと言う場合には、 Java の検査例外の構文が良くないのか、検査例外という概念そのものが良くないのかを区別して考えなければいけません。僕の考えは前者です。現に、 Swift は検査例外に似たエラー処理機構を取り入れ、うまく機能しています。

検査例外には二つの性質があります。一つはエラーの種類を静的型で扱うこと、もう一つはエラー処理を強制することです。 Swift でサポートされているのは後者で、前者については[議論が続いています](https://github.com/apple/swift-evolution/pull/68)。なので、 Swift で検査例外（的なもの）がうまく機能していると言っても、検査例外の『一部』がうまくいっているだけかもしれません。

しかし、僕は Swift で検査例外（的なもの）が機能している理由は静的型エラーをサポートしないからではない思います。 Swift で検査例外（的なもの）がうまくいっているのは、構文の改善によるところが大きいでしょう。

`defer` によって `finally` の煩わしさに対処し、ラムダ式・クロージャ式との相性の悪さを `rethrows` が解消しました。また、関数に `throws` が付与されている場合、呼び出し箇所に `try` の記述を強制することで _Implicit control flow problem_ を軽減しています。そして何より、 `try!` の存在が大きいです。

静的なチェックは万能ではありません。たとえば、文字列→整数の変換は失敗する可能性がある処理ですが、 UI 側で入力できる文字が数字に限定されているかもしれません。そのような文字列は必ず整数に変換できますが、コンパイラはそこまで理解できません。そんなときに、 _Simple domain error_ や _Recoverable error_ を _Logic failure_ として扱う簡単な構文が必要です。 `!` や `try!` がそれに当たります。これがないと、検査例外は時に不要なエラー処理を強制する煩わしいものと化してしまいます。

例えば、もし Java で `try!` と同じことをしようとすると次のようになります（ `FormatException` を `throw` し得る `toInt` というメソッドがあるとします）。

```java
int number;
try {
  number = toInt(string);
} catch (FormatException e) {
  throw new RuntimeException("Never reaches here.");
}
```

Swift なら次の通りです。シンプルですね！

```swift
let number = try! toInt(string)
```

絶対に起こらないエラーを _Logic failure_ 化するために、毎回 Java のようなコードが必要なら検査例外にうんざりすることでしょう。

## 検査例外とエラーの分類

しかし、今日の主題はエラーの分類です。 "Error Handling Rationale and Proposal" のエラー分類を理解するにつれ、僕は Java とのエラー分類の違いが、 Swift で検査例外が機能する理由の一つではないかという仮説を持つようになりました。

Java で `throw` できるものはすべて `Throwable` のサブタイプです。 `Throwable` のサブタイプには大きく分けて

- `Exception`
- `RuntimeException`
- `Error`

の三つがあります。

Java 公式チュートリアルや必読書と言われる Effective Java によると

- 回復可能なエラー→  `Exception`
- プログラミングエラー→ `RuntimeException`
- 実行継続不能な致命的エラー→ `Error`

という使い分けが推奨されています。

言いかえれば、

- _Recoverable error_ → `Exception`
- _Logic failure_ → `RuntimeException`
- _Universal error_ → `Error`

ということです。この使い分け自体は妥当だと思います。

しかし、 Java の設計当時、 Java の設計者たちはこの分類を明確に意識できていなかったのではないかと僕は疑っています。 Java 設計者の気持ちになって考えてみましょう。

今、検査例外という素晴らしい仕組みを導入し、エラー処理を強制することを考えています。しかし、すべてのエラー処理を強制しようとすると破綻します。たとえば、配列の要素にインデックスでアクセスするときにすべて `try` - `catch` を強制するのは非現実的です。 Java ではそのようなときに `RuntimeException` のサブクラスである `ArrayIndexOutOfBoundsException` が投げられます。 `RuntimeException` は _Logic failure_ なので Swift と同じです。何が問題なのでしょう？

僕は、設計当時 `RuntimeException` は _Logic failure_ ではなく「とても全部処理していられない頻度の高いエラー」と考えられていたのではないかと疑っています。全部処理するとコードがぐちゃぐちゃになってしまうので処理するかはプログラマに任せよう、と。

その証拠と僕が考えているのが、 `RuntimeException` （ _Logic failure_ ）が `Exception` （ _Recoverable error_ ）のサブクラスになっていることです。そのため、 `catch(Exception e)` と書くと `Exception` （ _Recoverable error_ ）だけでなく、サブクラスである `RuntimeException` （ `Logic failure` ）まで捕まえてしまいます。

しかし、 _Logic failure_ は最も回復『不能』なエラーです。決して実行時に処理してはいけません。 Effective Java にも `RuntimeException` は `catch` すべきでないと書かれています。それであれば `RuntimeException` を `Exception` のサブクラスにしたのは明確な誤りです。このエラー分類の曖昧さが標準ライブラリの設計に影響しているように思えます。

一番ひどいのは文字列→整数の変換です。このメソッド `parseInt` が投げるのは `RuntimeException` のサブクラスです。つまり、文字列→整数の変換失敗はcatchして処理してはいけないということです。

しかも、他に文字列を整数に変換できるかチェックするためのメソッドが提供されているわけでもありません。つまり、ユーザーが入力した不正文字列に対して「整数を入力して下さい」と表示するには、 _Logic failure_ を `catch` して回復処理を書くしかないのです。もし、初めから `RuntimeException` を _Logic failure_ だと考えていたら、このような設計はあり得なかったでしょう。 _Logic failure_ はコードの誤りであり、実行時に回復してはいけないのですから。

この他にも、 Java の標準ライブラリには引数が不正だった場合に `RuntimeException` を `throw` するメソッドがたくさんあります。何せ、 `IllegalArgumentException` という `RuntimeException` のサブクラスがあるくらいです。

標準ライブラリの設計に引っ張られてか、 Java の世界ではこれらを _Logic failure_ として扱うことを推奨しています。つまり、引数に前提条件があるメソッドに対して誤った値を渡すのは、前提条件のチェックを怠ったコードのバグだからコードを修正すべきだという主張です。

しかし、本当にそれらを _Logic failure_ として扱うべきでしょうか。 `Array` のインデックスがはみ出てしまうエラーが _Logic failure_ として妥当なのは、前提条件（はみ出ていないか）をチェックして分岐するようなコードがほぼ不要だからです。前提条件のチェックが頻繁に行われるなら、「不注意」によってチェックを忘れてしまうこともあるでしょう。せっかく「不注意」を防ぐために検査例外が導入されたのに、その「不注意」が（コンパイル時ではなく）実行時まで検出できないのでは何のための検査例外なのでしょうか。

~~前提条件が明確なエラーの多くは _Simple domain error_ として扱うのが適切です。~~ （→[追記にて訂正](#%E8%BF%BD%E8%A8%982017-01-18)） 最も回復が簡単な _Simple domain error_ を回復不能な _Logic failure_ にしてしまったエラー分類の曖昧さこそ Java の失敗だと僕は思います。そして、「不注意」によるエラー処理忘れを防げるという検査例外の大きな恩恵の一つを _Simple domain error_ について捨ててしまったがために、 Java の検査例外を使っていてもありがたみよりも煩わしさを感じる人が多いのではないでしょうか。

Swift は構文が優れているのはもちろんのこと、エラー分類が適切だからこそ安全かつ快適なエラー処理を実現できているのだと思います。「不注意」な僕にとっては最高の言語です！

## まとめ

- 人間は「不注意」なのでエラー処理忘れをコンパイラが検出できることは素晴らしい
- 静的なチェックは万能ではないので、 _Simple domain error_ 、_Recoverable error_ を _Logic failure_ 化する `!` や `try!` が重要
- Java では _Simple domain error_ が適切と思われるケースで _Logic failure_ が採用されており、「不注意」によるエラー処理忘れを静的に検出できる恩恵を受けづらい
- エラー分類が適切でなければ安全かつ快適なエラー処理は実現できない

# 追記（2017-01-18）

> [id:sawat](http://b.hatena.ne.jp/entry/316060189/comment/sawat) IllegalArgumentException が Logic failure じゃない場面なんてそんなに多くない気がするが…。

はてブでこのようなコメントをいただいて考えていたのですが、たしかにその通りなように思います。

Logic failure かそうでないかの境目は、エラーが発生したときにそれをハンドリングして何らかの処理を実行することがあるかどうかですが（ `Array` の index out of bounds を処理することが滅多にないので Logic failure としたのと同じように）、 **前提条件が明確な** `IllegalArgumentException` （引数に渡された不正な値が原因のエラー）であっても、それをハンドリングする必要があるようなオペレーションは下記のケースを除いてほとんど思い付きませんでした。

Simple domain error となるのは次のようなケースです（ `removeLast` メソッド等は引数が不正なわけではないですが、第一引数にオブジェクト自身を渡す関数と読みかえれば引数の不正と同じ話です）。

- 変換（例: `parseInt`, JSON のデコード等）
- 値の不在（例: `pop`, `Iterator` の `next` 等）
- 計算（例： ゼロ除算（単位ベクトルや逆行列の演算）、負の数の平方根）
- 数を引数にとる処理（例: 配列の生成）

値の変換はプログラムの外から来た値がインプットとなる場合が多いので、コードレベルで不正値を防ぐことができないケースが多く、エラーをハンドリングしたいです。コレクションに対する操作も、 DB から取得するなど外部から来たものを操作することが多く同様です。

計算や数を引数にとる処理は、オペランドや引数が計算の結果として導かれた結果、簡単に値が範囲外になってしまいます。 `sqrt(a - b)` の `a - b` が負であるような場合に、事前チェックが必要なのであれば、それは Logic failure ではなく Simple domain error であるべきです。コレクションのケースも同様に、他のオペレーションの結果としてコレクションを得た場合には、 `pop` する前に空でないことなどのチェックが必要となります。

これらに共通なのは、引数に渡される値をコードレベルでコントロールできない（コントロールするのが難しい）ことです。そのため、引数に不正な値が渡されるケースがコードの誤りとは限らず、 Logic failure ではなく Simple domain error として扱いたいわけです。

しかし、不正引数で引き起こされる他の多くのケースはエラーは捕まえてもどうしようもないことが多いように思います（ `MessageDigest ` の `getInstance` に未知のアルゴリズムを表す文字列が渡された場合など）。なので、

> 前提条件が明確なエラーの多くはSimple domain errorとして扱うのが適切です。

というのは **誤り** だと言えそうです。訂正します。

ただ、 Simple domain error を発生させるような処理は利用頻度が高く、それらが安全にハンドリングできないことによるストレスは大きいとは思います。 Java には Simple domain error を安全かつ快適に扱う良い方法がなく（ try-catch は大がかりすぎますし、 `null` を返しても `null` チェックを無視できてしまうので安全でありません。しかも、 `null` はネストできないので、たとえば `Iterator` の `next` が `null` でエラーを表すと、次の値が存在しないのか、次の値が `null` なのかを区別できません。）、 Simple domain error であるべきエラーが Logic failure として扱われているという点自体は間違いではないので、全体的な論旨はおかしくないと思います。