はじめに
基礎的な内容だが、Rails を使ったWebアプリケーションにおける例外処理に対する私見を整理する。
業務エラーとシステムエラーとで処理方法を使い分ける
業務エラー
業務設計上発生することが想定されるエラーのこと。
具体的には、ユーザーの入力値によるエラーのようなユーザーに次のアクションを取らせるエラー。
業務エラーのハンドリングに例外を使うと、処理の流れが追いにくくなるので基本的には分岐処理で制御する。
❌Bad
begin
raise ArgumentError, "入力値が無効です" unless user.valid?
# ...
rescue ArgumentError => e
# ...
end
✅Good
if user.valid?
# ...
else
# ...
end
あるいは Result Object
パターンを使う。
手続き的なフロー制御において、各ステップの処理結果の成否を判定→パイプチェーンするような構造のビジネスロジックでは役立つ。
def ...
# 成功のケース
if ...
return Result.new(success: true, value: value)
...
# 失敗のケース
if ...
return Result.new(success: false, error: error)
end
result = ...
if result.success?
# ...
else
# ...
end
例外を使うケース
ただし、例外を発生させるメリットが大きければ例外使う。
例えば、ユーザーが指定したデータが既に存在しない場合はActiveRecord::RecordNotFound
を発生させるとRack層の標準のエラーハンドリングに任せることができて楽ができる。
システムエラーとは
DBへの接続エラー、来るはずがない分岐処理に来た場合など、開発者が対応しなければ解決しないエラー。
- Sentry などエラートラッキングツールを導入しておき、Webフレームワーク標準のエラーハンドリングに委ねる。
- 動的なエラーレスポンスを返すなどの場合はカスタムRack Appを作り、middlewareとして登録する。
rescue_from
について
rescue_from は共通的なエラーハンドリングのために使われることが多い印象。
そのエラーの中に StandardError
を含めてしまっていることがあるが、これは推奨された方法ではない。
rescue_fromでExceptionやStandardErrorを指定すると、Railsの正常な例外ハンドリングが阻害されて深刻な副作用が生じる可能性があります。よほどの理由がない限り、このような指定はおすすめできません。
Action Controller の高度なトピック - Railsガイド
深刻な副作用
とは?
If you rescue from StandardError things like automatic the 404 for ActiveRecord::NotFoundError and RoutingErrors will not happen anymore, so now your application have to know about every single exception that the framework that raise and have special meaning.
(StandardError をキャッチすると、ActiveRecord::NotFoundError や RoutingErrors に対する自動的な 404 エラー処理などが機能しなくなります。このため、アプリケーション側でフレームワークが発生させるすべての例外とその特別な意味を個別に把握しておく必要が生じます。)
https://github.com/rails/rails/issues/33390#issuecomment-406069169
その他にも、Rack Middleware に処理を差し込むタイプのgem(Sentry, Datadog, logrageなど)が機能しなくなるリスクがある
StandardErrorをrescueするケースについて
rescueする時は可能な限り例外クラスを列挙すべきだが、種類が多すぎて網羅できない場合があるので、StandardErrorを捕捉する場合もありうる。
他にも、途中でエラーが発生しても処理を続行したい場合も同様である。
ex. メール一斉送信処理で途中でエラーになるユーザーがいても、中断せずに残りのユーザーに送信させる
ただし、rescueした時は必ずログ出力や必要に応じて開発者への通知処理も欠かさないこと。