Rails例外処理大全
#rails
まとめ
rescue StandardError使ったら負け
コントローラのアクション内をすべて拾うのもだいたい負け
業務エラーとシステムエラーを使い分ける
業務エラー
ユーザーの誤入力など、ユーザーに正しく入力し直させるためにフィードバックする必要があるエラー。ステータスコードは400系になる
バリデーションエラー
if @post.save ~ else ~ の形で基本的にOK
code:post_controller.rb
def create
if @post.save
# 成功時の処理 / redirectとか, ユーザーにメール通知するとか
else
# 失敗時の処理 / @post.errorsを用いてユーザーにフィードバックする
end
end
基本的に更新系のcontrollerは上記のように終わるように書いておけば良い
ActiveModel::Errorsを活用する
権限系のチェック
https://github.com/varvet/pundit 等
authorize! メソッドを呼び、ハンドリングは外側のrescue_fromに任せると楽. RecordNotFound時のハンドリングに近い
その他
ExceptionWrapperで拾われるやつ
https://github.com/rails/rails/blob/f2caed1e73a3a781de5f26766935947448c47aac/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb#L8
https://github.com/rails/rails/blob/45d1efab5163f030a1a8555d44ab11b4cd437d13/activerecord/lib/active_record/railtie.rb#L22-L27
このマッピングで自動的に落ちてくれる
not found
ActiveRecord::RecordNotFoundが404に落ちてくれるのは有名 ( User.find() や find_by! などを使って見つからなかったときに発生する)
forbidden, unauthorized
ユーザー定義で増やすこともできる
握りつぶされるやつ https://github.com/rails/rails/blob/f2caed1e73a3a781de5f26766935947448c47aac/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb#L39
システムエラー
システム開発者側が何らかの対応をする必要がある、エラー発生時にはどうしようもできないエラー。ステータスコードは500系になる
起きたら盛大にstatus code 500で死ぬ方針
ユーザーにはどうしようもできないが、エラーが起きたことだけはフィードバックする (エラー犬)
開発者が異常が起きていることを知ることができる設計にしておく
applicationレイヤー内で捕捉しない
code:ho.rb
class HogeController
rescue_from StandardError do |e|
# DON'T こういうのとかやめる. するなら必ず再度raise eして外側にぶん投げる
end
def destroy
# -----
@post.destroy!
rescue
# DON'T ↑こういう例外クラス指定なしで捕捉するのをやめる. メソッド内のStandardErrorが拾われ握りつぶされる
# 実際意図しているのはActiveRecord::RecordInvalidError だったりする場合が多いが、StandardErrorを拾うとNoMethodErrorやArgumentErrorなどのエラーも拾われる
flash:error = 'エラーが起きました'
end
end
まずエラー起きません、というときは例外投げる設計にしておいたほうが親切
上記では@post.destroy!は必ず成功する前提であればrescue要らない. 条件によって失敗するなら前述の業務エラーのハンドリングを使う
例えば@post.destroy!する前にポストの公開状態を非公開にしてからでないといけない、みたいな要件は、modelのバリデーションに書けるだろう
エラー通知ツール/gemに捕捉させる
https://github.com/getsentry/sentry-ruby
https://github.com/rollbar/rollbar-gem
このあたりは入れるだけでrack層でエラーを捕捉してくれる
その他tips
前提条件を絞るためにbefore_actionでふるい落としておくと良い
よくある例だと nested resourceの場合 
/posts/:post_id/comments みたいなとき
before_action :set_post みたいなの書く (ここでは Post.find(params[:post_id] するだろうから、ActiveRecord::RecordNotFoundがraiseされ、 -> 404に落ちる)
@post が存在することを前提とできるのでアクション内のnilチェックを減らせる
他、requestのcontent typeで絞っておくとか
パラメータがおかしい(パースできないとか)ときbadrequestに落としておくとか
失敗した場合にいくつかのパターンをハンドリングする必要があるときは例外を使わず、結果を値として扱っても良い
code:_a.rb
HogeResult = Data.define(:errors) do # errorsの型は場合によって要検討。
def success? = errors.blank?
end
# --こういうクラスを定義して
def complex_method(aaa)
# なんか難しい処理
if xxx
# なんかの処理に失敗した
return HogeResult.new(errors: 'XXに失敗', 'YYに失敗') # ここはエラーコードを定義などしてもOK
end
if yyy
# 別の処理に失敗した
return HogeResult.new(errors: 'ZZZに失敗')
end
return HogeResult.new(errors: nil)
end
# ↓コントローラでこう使う
class HogeController < ApplicationController
def create
result = complex_method(create_params)
if result.success?
render json: { success: true }, status: :ok
else
if result.errors.include?('XXに失敗')
# このエラーの場合だけユーザーに通知する
HogeFailedMailer.deliver_later(user, 'XXの形式が正しくありません 再度やり直してね')
end
render json: { success: false, errors: convert_to_api_errors(result.errors) }, status: :bad_request
end
end
end
2024/04/13追記 memo
https://railsguides.jp/error_reporting.html に追従できていない