宗教論争 例外処理はどこで行うべきなのか?

『自走プログラマー ~Pythonの先輩が教えるプロジェクト開発のベストプラクティス120』を読んでいて、ふと昔同僚とやった例外処理の議論を思い出したので書いてみる。

議題: 例外処理は一番上で行うべきなのか、それともできるだけ下で行うべきなのか

例外処理はどこで行うのが適切か。Controller層あるいはFrontControllerと呼ばれる箇所でtry-catch(Pythonだとtry-except)を書くべき(上で書くべきだよ派)なのか、それとも各ModelやServiceと呼ばれるロジック層で書くべき(下で書くべきだよ派)なのか。

話したときは、たしか"これは宗教論争だ。個人の好みでは。"ということで終わったと記憶しているが、本当にそうか。

結論: ケースバイケースだが、基本、両方。

上にプロジェクト共通の処理を書く必要がある(まあ大抵は)

生の例外をそのまま言語やフレームワークのデフォルトの扱いに任せていいプロジェクトであれば、そうすればいい。

しかしログを残したいとか、エラーメッセージを直で返すのはいくらなんでも慎みに欠ける(エンドユーザー向けだといよくある判断)ようなケースはそうはいかない。

何が起きてもいいように、例外の最上位クラス(Exception)ですべての例外をキャッチして、既定の処理をかける必要がある。

だから、少なくとも上にtry-catchか、それに近いことが行えるものを設置するのは必須だろう。ASP.NET Coreだと例外ハンドラーという専用のメソッドがあるのでtry-catchではないのだが、考え方は同じと思う。

    // これはpythonではなくてC#
    if (app.Environment.IsDevelopment())
    {
        app.UseExceptionHandler("/error-development");
    }
    else
    {
        app.UseExceptionHandler("/error");
    }

下にも必要に応じて書く必要がある。

一番上に書いていれば例外処理は十分かというとさにあらず。

よくあるのはDBのトランザクション処理周りである。もしサービス層でトランザクションのコードを書くスタイルだと、おなじ場所で例外処理をかけることが多い。

# これは実際のコードではない。雰囲気を伝えるのにそれっぽく書いているだけ。
try: 
  con.begin()

  # (何かの処理)
  con.commit()
except: 
  con.rollback()

また、返り値を返すのではなく例外を投げるタイプの関数を呼んでいて、かつそこで例外を投げたくない場合もあるだろう。

try: 
  result = executeOrRaise()
except: 
  pass

if result <> "SUCCESS":
  recover()

こんな場合でも、例外を握りつぶさないといけないので、やっぱり必要になる。

結局、上と下の両方で必要に応じて書かないとだめということである。