/note/tech

エラー処理の温故知新

要約:

■ 1. 概要

  • 発表者: Nakaya Ryota(株式会社ギフティ バックエンドエンジニア)
  • 発表日時: 2026年05月01日 giftee TechBash
  • テーマ: エラー処理の歴史的変遷と各言語の設計思想の比較

■ 2. エラーの定義と分類

  • エラーの定義:
    • エラーとはプログラムの期待と現実のギャップである
    • プログラムが置いた前提や仮定が崩れた瞬間がエラーである
    • 前提は必ずどこかで崩れるためエラー処理は不可避である
  • エラーの発生原因:
    • ファイルが存在しない
    • ネットワーク障害
    • 入力値の不正
    • NULLポインター
    • 型の不一致
  • エラーの分類:
    • ドメインエラー: 入力値の不正 / 実行前提条件の未充足 / 認証・認可の失敗
    • 非ドメインエラー: メモリ不足(OOM) / ネットワーク障害 / NULLポインター参照

■ 3. C言語の時代 — 値としてのエラー

  • C言語とUnix哲学においてエラーは「値」であり プログラムのロジックで処理するものとして扱われた
  • 関数やプログラムは成功・失敗を数値で表現する(0なら正常 0以外なら異常)
  • 利点:
    • シンプルな記述が可能
  • 欠点:
    • 戻り値のチェックを忘れるリスクがある
    • 正常系と異常系のコードが混在する

■ 4. Exceptionの時代

  • 概要:
    • C++ / Java / Python / Rubyなど現代主流の言語で採用される方式
    • 例外機構自体は以前から存在していたが 90年代の言語普及が広めた
  • 利点:
    • 正常系と異常系のフローを分けて記述できる
    • 大域脱出するためチェック漏れのリスクがない
  • 課題:
    • どこでどういう例外が発生するのかを関数シグネチャから把握できない
    • プログラム全体を見ないとハンドリング箇所が把握できず設計難易度が高い
    • スローされたまま誰もキャッチしない事態が生じる
    • 意図せず握りつぶされるリスクがある

■ 5. Javaの検査例外

  • 概要:
    • 関数がスローしうる例外をシグネチャで明示する仕組み
    • キャッチして処理するかスローを伝播するかのどちらかを選ばないとコンパイルが通らない
    • 処理漏れをコンパイル時に防止できる
  • トレードオフ:
    • 伝播コスト: 全呼び出し層でキャッチ・スローを記述する必要があり ラップしてリスローするコードが量産される
    • API変更への脆弱性: 例外の種類を増やすと呼び出し側全員がキャッチ処理を追加する必要がある
    • 高階関数やラムダ式など関数型記法との相性が悪い
    • 失敗可能性の明示と失敗からの回復可能性は別問題であるという本質的な課題がある

■ 6. Goのエラーハンドリング — 値への回帰

  • 概要:
    • 2009年に登場したGoはエラーを値として返す古典的方法を意図的に選択した
    • "Errors are values" — Rob Pike の思想が基盤にある
  • 設計思想:
    • 例外は使用しない(panicは回復不能な異常時のみ使用)
    • エラーは日常的な事象であり 特別な制御構造ではなく通常の値として扱う
    • 常にその場で処理するか 呼び出し元に返すかを明示する
    • 「魔法を減らせ」という哲学に基づく
  • 値リターンへの回帰の理由:
    • Exceptionはシグネチャに現れず制御フローが追いにくい
    • throw元とcatch先が離れるためデバッグが困難("Cleaner more elegant and wrong" — Raymond Chen)
    • 暗黙の大域脱出が隠れており制御フローの把握が難しい
    • ドメインエラーはロジックで処理し 非ドメインエラー(OOM / NPE)は大域脱出とする使い分けが可能
    • gotoと同様に例外だけを特別扱いする設計への疑問

■ 7. オフトピック: Clean Codeの見解

  • 『Clean Code』の著者は値リターンはコードの可読性を下げるためお勧めしないと記している
  • 正常系がストレートに読める方がコードの意図が掴みやすいとされる
  • ただしイディオムの違いによる可読性の議論は宗教論争になりがちである

■ 8. 型によるエラー表現

  • 概要:
    • Rust / Haskell / Scala / Swift / Kotlinなど近年の言語で採用される方式
    • 失敗する可能性を型システムで表現する(例: Result<String, Error>
    • 成功値または失敗値のどちらかが返ることが型レベルで保証される
  • 値としての扱い(Go)vs 型としての扱い(Rust)の違い:
    • Goの値: errを無視してもコンパイルは通り チェックは開発者の自己責任
    • Rustの型: Resultを開かないと中身を使えず コンパイラが処理を強制する
  • Result型のハンドリング(Rust):
    • matchで全パターンを網羅しないとコンパイルエラーとなる
    • Okケースのみ記述してErrを省略するとコンパイルエラー
    • 「処理し忘れ」がそもそも起こりえない構造になっている
    • 意図的に捨てることは可能(let _ = read_file();
    • Javaの検査例外と異なり どの階層で処理するかは開発者が自由に選べる
  • 型によるエラー表現の特徴:
    • うっかり無視はできないが意図的に捨てることはできる
    • Java検査例外: 各階層に「処理または宣言」を強制
    • RustのResult: 失敗の可能性を示すが どこで処理するかは自由
    • コンパイラが処理漏れを検出できる(例外より明示的)
    • エラーを合成でき関数型との相性が良い
    • 記述量が増える場合がある

■ 9. まとめ

  • エラー処理の仕組みと思想は言語ごとに異なる
  • 最近の言語トレンドはエラーを型で表現する方向に向かっている
  • それぞれの特性を理解して正しく向き合うことが重要
  • エラー処理に銀の弾丸はない