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

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

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

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

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


- 参考 URL 等
    - [項目22 | Effective Ruby（長尾高弘 arton PeterJ\.Jones）｜翔泳社の本](https://www.shoeisha.co.jp/book/detail/9784798139821)
    - [Railsアプリケーションにおけるエラー処理（例外設計）の考え方 \- Qiita](https://qiita.com/jnchito/items/3ef95ea144ed15df3637)
    - [【Rails】カスタム例外の定義とそのハンドリング（カスタム例外を使えと言っている訳ではない） \- Qiita](https://qiita.com/luigiii/items/760686bf35d2646588ae)
    - [【Ruby】カスタム例外を作成する簡単３ステップ \- Qiita](https://qiita.com/k-penguin-sato/items/3d8ce4e71520da2d68c4)


# □ 本文
## ■ 前提情報

### 使用するアプリケーション
Railsチュートリアルで作成する SampleApp における、users_controller の`users#edit`に着目して実装します。

```ruby: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
```


- [sample\_apps/users\_controller\.rb at master · yasslab/sample\_apps](https://github.com/yasslab/sample_apps/blob/master/5_1_2/ch14/app/controllers/users_controller.rb#L92-L97)

### カスタム例外の設定規則

- `StarndardError` を継承する
- クラス名の末尾に`Error`を付ける


## ■ カスタム例外の設定方法

実装自体は，とても単純なのですが，設定場所にいくつか種類がありますので，紹介していきます。

### 実装例１: 発生ファイルに直接設定

その名の通り，例外が発生するファイル自身に設定します。

特定のクラスに強く結びつける方法であることから，Model 層や Service 層で見かけたりします。

```ruby: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
```



### 実装例２: app/ 配下に設定

自作の module を app/ 配下に設定します。

validators や services と同じ考え方で配置する感じですかね。

```ruby:app/errors/application_error.rb
module ApplicationError
  class NotPermittedError < StandardError; end
end
```


```ruby: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
```


### 実装例３: lib/ 配下に設定

lib ディレクトリの存在目的から見ると，王道パターンかも。

なお，lib 直下の配置が気になる場合，適宜ディレクトリを挟んで設定してください。


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

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

module SampleApp
  #...
end
```


```ruby:lib/exception.rb
module Application
  class Error < StandardError; end
  class NotPermittedError < Error; end
end
```

```ruby: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](https://github.com/masayuki-0319/sample_app/pull/9)