Rails で例外を発生させたい際は,raise
...つまり RuntimeError をよく使用するかと思います。
しかし,サービス上の制約から,特定の状況下で例外を発生させる場合,raise
だけでは物足りなくなる時があります。
raise
では「何かまずいことが起きてしまいました!」程度のことしか伝えてくれません。まぁ,引数に渡す message を見れば理解できるかもですが...
兎にも角にも,特定の状況下に対する例外が存在するなら,その例外に対して名前を付けてあげましょう。
カスタム例外を設定すると,発生時に「何に対する例外か」がパッと理解できるようになりますし,特定の動作に誘導することも容易になりますので,良いことづくめです!
- 参考 URL 等
□ 本文
■ 前提情報
使用するアプリケーション
Railsチュートリアルで作成する SampleApp における、users_controller のusers#edit
に着目して実装します。
class UsersController < ApplicationController
before_action :correct_user, only: [:edit, :update]
#...
private
#...
# 正しいユーザーかどうか確認
def correct_user
# GET /users/:id/edit
# PATCH /users/:id
@user = User.find(params[:id])
redirect_to(root_url) unless current_user?(@user)
end
#...
end
カスタム例外の設定規則
-
StarndardError
を継承する - クラス名の末尾に
Error
を付ける
■ カスタム例外の設定方法
実装自体は,とても単純なのですが,設定場所にいくつか種類がありますので,紹介していきます。
実装例1: 発生ファイルに直接設定
その名の通り,例外が発生するファイル自身に設定します。
特定のクラスに強く結びつける方法であることから,Model 層や Service 層で見かけたりします。
class UsersController < ApplicationController
class NotPermittedError < StandardError; end
before_action :correct_user, only: [:edit, :update]
#...
private
#...
def correct_user
@user = User.find(params[:id])
raise NotPermittedError, "あなたにリクエスト権限がありません" unless current_user?(@user)
end
#...
end
実装例2: app/ 配下に設定
自作の module を app/ 配下に設定します。
validators や services と同じ考え方で配置する感じですかね。
module ApplicationError
class NotPermittedError < StandardError; end
end
class UsersController < ApplicationController
before_action :correct_user, only: [:edit, :update]
#...
private
#...
def correct_user
@user = User.find(params[:id])
raise ApplicationError::NotPermittedError, "あなたにリクエスト権限がありません" unless current_user?(@user)
end
#...
end
実装例3: lib/ 配下に設定
lib ディレクトリの存在目的から見ると,王道パターンかも。
なお,lib 直下の配置が気になる場合,適宜ディレクトリを挟んで設定してください。
#...
Bundler.require(*Rails.groups)
# ↓ 追加コード
require_relative '../lib/exception.rb'
module SampleApp
#...
end
module Application
class Error < StandardError; end
class NotPermittedError < Error; end
end
class UsersController < ApplicationController
before_action :correct_user, only: [:edit, :update]
#...
private
#...
def correct_user
@user = User.find(params[:id])
raise ApplicationError::NotPermittedError, "あなたにリクエスト権限がありません" unless current_user?(@user)
end
#...
end
■ エラーハンドリング
さて,これでカスタム例外は作成完了ですが,仕上げが残っています。
このままでは,例外を発生させたままです。
ユーザ側から見ると 500 エラー画面が出てきて,なぜ強制終了したのか理由がわかりませんし,サービスの操作感として連続性が失われるのも避ける必要があります。
元のコードでは,不正なアクセスをしたユーザに対して,root_url にリダイレクトさせていますので,カスタム例外が発生した際は,同じようにリダイレクトさせましょう。
また、例外を握り潰さないために、サーバ側に理由を説明するためのログを残しましょう。
class ApplicationController < ActionController::Base
#...
rescue_from Application::NotPermittedError, with: :redirect_root_page
def redirect_root_page
Rails.logger.info "ルート URL にリダイレクト: #{exception.message}" if exception
redirect_to root_url, flash: { danger: "閲覧権限がありません" }
end
#...
end
※ シンプルに表記することを目的として application_controller.rb に記入していますが,色々追加されるファイルでもあるため,concerns に切り出すと尚可読性が高まるでしょう。
□ 余談
OSS におけるカスタム例外の設定方法も調べてみると,見事にバラバラだったので,プロジェクト毎に設定方法が異なるかもしれない
具体的な命名も設定場所も異なるため、プロジェクトに合わせて、柔軟に対応しましょう。
今回使用した PR です→カスタム例外の設定 by masayuki-0319 · Pull Request #9 · masayuki-0319/sample_app