LoginSignup
27
21

More than 3 years have passed since last update.

Rails によるカスタム例外の設定とエラーハンドリング

Last updated at Posted at 2020-06-21

Rails で例外を発生させたい際は,raise...つまり RuntimeError をよく使用するかと思います。

しかし,サービス上の制約から,特定の状況下で例外を発生させる場合,raiseだけでは物足りなくなる時があります。
raiseでは「何かまずいことが起きてしまいました!」程度のことしか伝えてくれません。まぁ,引数に渡す message を見れば理解できるかもですが...

兎にも角にも,特定の状況下に対する例外が存在するなら,その例外に対して名前を付けてあげましょう。

カスタム例外を設定すると,発生時に「何に対する例外か」がパッと理解できるようになりますし,特定の動作に誘導することも容易になりますので,良いことづくめです!

□ 本文

■ 前提情報

使用するアプリケーション

Railsチュートリアルで作成する SampleApp における、users_controller のusers#editに着目して実装します。

app/controllers/users_controller.rb
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 層で見かけたりします。

app/controllers/users_controller.rb
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 と同じ考え方で配置する感じですかね。

app/errors/application_error.rb
module ApplicationError
  class NotPermittedError < StandardError; end
end
app/controllers/users_controller.rb
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 直下の配置が気になる場合,適宜ディレクトリを挟んで設定してください。

config/application.rb
#...
Bundler.require(*Rails.groups)

# ↓ 追加コード
require_relative '../lib/exception.rb'

module SampleApp
  #...
end
lib/exception.rb
module Application
  class Error < StandardError; end
  class NotPermittedError < Error; end
end
app/controllers/users_controller.rb
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 にリダイレクトさせていますので,カスタム例外が発生した際は,同じようにリダイレクトさせましょう。

また、例外を握り潰さないために、サーバ側に理由を説明するためのログを残しましょう。

app/controllers/application_controller.rb
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 におけるカスタム例外の設定方法も調べてみると,見事にバラバラだったので,プロジェクト毎に設定方法が異なるかもしれない :thinking:

具体的な命名も設定場所も異なるため、プロジェクトに合わせて、柔軟に対応しましょう。

今回使用した PR です→カスタム例外の設定 by masayuki-0319 · Pull Request #9 · masayuki-0319/sample_app

27
21
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
27
21