[Swift]Error処理を見直す

## 始めに
開発をしていると、エラー処理に悩む場面がたびたびあります。
そこで、一度基本に立ち返り、エラー設計や処理方法について学び直してみることにしました。

## Errorとは
まずは以下の書籍などを参考に、エラーの基本的な考え方を整理しました。

https://www.amazon.co.jp/Good-Code-Bad-%EF%BD%9E%E6%8C%81%E7%B6%9A%E5%8F%AF%E8%83%BD%E3%81%AA%E9%96%8B%E7%99%BA%E3%81%AE%E3%81%9F%E3%82%81%E3%81%AE%E3%82%BD%E3%83%95%E3%83%88%E3%82%A6%E3%82%A7%E3%82%A2%E3%82%A8%E3%83%B3%E3%82%B8%E3%83%8B%E3%82%A2%E7%9A%84%E6%80%9D%E8%80%83/dp/4798068160/ref=sr_1_2_sspa?adgrpid=145509061632&dib=eyJ2IjoiMSJ9.ljw5PYq0Eo7U37ubbIMsufFzmBQaFd7xuEDGHaOSciRytNvto4xY6VCMgMlWCwWOQjislsqa1WhAMu4QatlAEOrk1uwazsyfWEPw5vVxIOjzobjhoYJFJSWfs_-GUCBRt0Wcq8hLLKnTkE-q2eLkHswmfGmkbst75yb7PMErl8a2wH9Rttl6hxtVVtaLY0bF.-rn4x9NPRTNLnDuGIhFTAWW-EyLJU_0I6kxZmJYavbo&dib_tag=se&hvadid=679067593430&hvdev=c&hvlocphy=1009260&hvnetw=g&hvqmt=e&hvrand=1213790237335071795&hvtargid=kwd-1634696925346&hydadcr=2017_13657078&jp-ad-ap=0&keywords=good+code+bad+code&mcid=237b71a5707e39bda12d14c5e3b6aadb&qid=1745994253&sr=8-2-spons&sp_csd=d2lkZ2V0TmFtZT1zcF9hdGY&psc=1

### 回復可能なエラー
* ユーザー入力ミス
* 一時的なネットワークエラー
* 非致命的な処理失敗（例：ログ送信失敗など）

💡 対応方法：処理継続・ユーザーへの案内・リトライ

### 回復不可能なエラー
* 致命的な不整合（例：必須ファイルの欠落）
* 想定外のアプリ状態

🧯 対応方法：ログ出力・クラッシュレポート・アラート表示などでエンジニアに気づかせる

### 回復可能かどうかの判断は「文脈」による
同じエラーでも、どの層で発生するか／どう扱うかによって「回復可能かどうか」は変わります。
よって、エラーは呼び出し元で判断すべきであり、関数側は「発生した事実」だけを知らせることが重要です。

### エラーの伝え方
#### ✅ 早めに失敗させる
早い段階で不正を検出すれば、後から原因を追う手間が省けます。

```swift
// ❌ 遅れてクラッシュするパターン
func start() {
    performMainTask(input: "abc") // Int変換できない値
}

func performMainTask(input: String) {
    stepThree(raw: input)
}

func stepThree(raw: String) {
    let number = Int(raw)! // ☠️ ここでクラッシュ
    print("処理中... \(number)")
}
```

```swift
// ✅ 早期にバリデーション
enum InputError: Error {
    case invalidNumber
}

func start() {
    do {
        try performMainTask(input: "abc")
    } catch {
        print("❗️エラー: \(error)")
    }
}

func performMainTask(input: String) throws {
    guard let number = Int(input) else {
        throw InputError.invalidNumber
    }
    stepThree(number: number)
}

func stepThree(number: Int) {
    print("処理中... \(number)")
}
```

#### 🧱 堅牢性とクラッシュのバランス
* クラッシュはエンジニアに確実に気づかせる手段
* ただし、堅牢性とのトレードオフ
* クラッシュを避けるなら、**ログやモニタリングで確実に記録する**ことが重要

#### 🙈 エラーを隠さない
エラーを「なかったこと」にすると、将来的なバグや不具合に気づきにくくなる原因になります。

* 🔁 デフォルト値を返す

```swift
// 🙅‍♀️ 無効なURLでも気づかずに処理が進んでしまう
let url = URL(string: userInput) ?? URL(string: "https://example.com")!
```

```swift
// 🙆‍♀️ フォールバックの意図を明示し、ログを残す
func urlFromUserInput(_ input: String) -> URL {
    if let url = URL(string: input), url.scheme?.hasPrefix("http") == true {
        return url
    } else {
        print("⚠️ 無効なURL入力: \(input) → デフォルトURLを使用")
        return URL(string: "https://example.com")!
    }
}

// 使用例
let userInput = "ht!tp://broken-url"
let url = urlFromUserInput(userInput)
print("使用するURL: \(url)")
```

* 🕳 空または nil を返す
静かに無視する処理は、本当に問題ないケースだけに留めるべきです。

```swift
func updateData(_ newValue: String?) {
    guard let value = newValue else { return } // 👈 無言で終了
    // 更新処理…
}
```

* 🚫 何もしない（黙殺）
何らかの失敗を完全に無視することは、原則として避けるべきです。


## 実践で悩んだこと
### 🧐 回復不可能なエラーはどう扱うべきか？
クラッシュによる強制終了はユーザー体験の観点から望ましくないため、アプリは継続動作させつつ、開発者が確実に把握できるよう、エラーログの出力を徹底する方針としました。

また、エラーコードの一覧をチーム内で共有し、すべてのエラー画面に共通のコードを表示する仕組みを導入しました。これにより、バグの特定がしやすくなり、開発者間・非開発者間を問わず、スムーズに報告・共有が行えるようになった点も非常に有用でした。

### 🎯 例外の粒度がわからない
例外の設計において、低レイヤーの詳細（例：SQLException）をそのまま上位に伝えてしまうと、抽象度が一致せず扱いにくくなると感じました。

以下の引用にもある通り、例外チェーンのような仕組みを用いて、上位には抽象的な例外を伝えつつ、ログ上では Caused by のような形で詳細な情報を保持しておくことで、デバッグのしやすさと設計の整合性を両立できると思いました。
> チェック済み例外が、実装の詳細を不用意にさらけ出している
>
>データベースやファイルとは何の関係も無さそうなのに、メソッドがSQLExceptionやIOExceptionを投げるのを何度も見たこと（あるいは書いたこと）がありませんか？ あるメソッドの初期実装で投げられる可能性のある例外を単純に全て集め、そのメソッドのthrows文に加えてしまうのは、開発者にとってはごく普通のことです（多くのIDEではこの作業をするように促しさえします）。このパス・スルー手法の問題はBlochの43項に違反してしまうのです。つまり**投げられる例外は抽象化レベルですが、その例外を投げるメソッドとは一貫性がありません**。
>
>**ユーザー・プロファイルをロードするのが仕事のメソッドは、ユーザーが見つからない時にはNoSuchUserExceptionを投げるべきであって、SQLExceptionを投げるべきではありません**。SQLExceptionを投げてしまうと、呼び出し側はユーザーが見つからないことは分かるかも知れませんが、SQLExceptionをどうすべきかが分かりません。**下にある失敗の詳細（例えばスタック・トレースなど）を投げ出すことなく、より適切な例外を投げるためには例外チェーンを使います**。それによって抽象化レイヤーがデバッグに有用な情報を保ちつつ、抽象化レイヤーより上の層と抽象化レイヤーよりも下にある詳細を分離できるようになります。

https://web.archive.org/web/20060908183942/http://www-06.ibm.com/jp/developerworks/java/040618/j_j-jtp05254.html#resources

### エラーログについて
全層でエラーログをPrintするとノイズになるので、気をつける必要があります。
また、全てのログでError表示することも重要なエラーがわかりづらくなるので、Warning(⚠️), Info(ℹ️)などを使い分けられるようにしておくと良さそうです。

以下のような情報を残しておくと、デバッグがしやすいのかなと思います。
* いつ（時刻・タイムゾーン）
* どこで（クラス名・ファイル・行数）
* 何が（エラー内容）

> このような教科書との乖離は業務アプリケーションという限定的な視点だからです。コンシューマ向けのゲームやスマホアプリとは異なり、バッチやサーバサイドのエラーログはユーザでは無く、エンジニアが障害調査等のためにみるものです。そのため、「ファイルがありません」 という形でスタックトレース等のエラー情報を丸めるのは確かに不適切です。一方で、ゲームやスマホのアプリ等の場合は、障害調査のための情報をユーザに見せる必要はありませんし、java.io.FileNotFoundException: no-file (No such file or directory)等の謎の呪文では無く適切なエラーメッセージを返すべきでしょう。**エラー情報は 「誰が」 見て、「何に」 使うのか、という点を意識するべきです。**

https://zenn.dev/koduki/articles/e9373cb78fcfef#%E4%BE%8B%E5%A4%96%E3%81%AF%E5%8E%9F%E5%89%87%E3%82%AD%E3%83%A3%E3%83%83%E3%83%81%E3%81%97%E3%81%AA%E3%81%84

> ・いつ（時刻、タイムゾーン）で発生したか
・どこ（システムの部分）で発生したか
・なにが発生したか
・（そして理想としては）日本語で書いてある

https://note.com/hisaotsu/n/n2f2a1ef25222

### ⛔️ 制御フローに例外を使わない
```swift
try? someCheck() ?? defaultValue
```
「普通の分岐」に例外を使うのは避けるべきかなと思います。
あくまで「異常系」のための設計に限定するべきです。

https://zenn.dev/koduki/articles/e9373cb78fcfef#%E4%BE%8B%E5%A4%96%E7%9A%84%E7%8A%B6%E6%85%8B%E3%81%AB%E3%81%AE%E3%81%BF%E4%BE%8B%E5%A4%96%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%99%E3%82%8B

### ❓ unknownError っていつ使う？
Claude AI で聞いたところ、以下のようなケースで使用されるとのことでした。
面倒臭いはよくない🙅‍♀️

* 予期しないサーバーレスポンス
* 既知のエラーコードに一致しないエラーが発生した時
* レスポンスのパースに失敗した時
* 通信エラーなどの低レベルエラーが発生し、具体的な原因を特定できない時
* サードパーティAPIが予期せぬレスポンスを返した時
>
>つまり、事前に定義されたエラーケースに当てはまらない「予期せぬエラー」の総称として使われることが多いです。これは開発者にとって **「何か問題が起きたが、具体的な原因は特定できていない」という情報を伝えるためのもの**です。
API設計の観点では、できるだけ具体的なエラーを返し、unknownError はどうしても分類できない場合の最終手段として使うのがベストプラクティスです。

## まとめ
エラーを適切にキャッチし、詳細な情報を記録しておくことで、バグの特定や修正が格段にしやすくなることを実感しました。
今後もエラー処理を軽視せず、設計段階からしっかりと向き合うことを意識していきたいと思います。