ぎんさんマインド

いちエンジニアの思考とか趣味についてつらつらと書いてみるかもしれない。

Railsにおけるエラーハンドリング

序文

について教えようとしていた際にRailsチュートリアルなどで記載がなかった。
一応例として
Error Handling in Rails — The Modular Way | by Sudharsanan Muralidharan | Rails, Ember & Beyond | Medium
があったが、古いのと英文であるため、似ている部分が多いが残しておきます。

本文

例えばユーザーを取得する際にControllerではこのように記載するのがメジャーだと思います。

class UserController < ApplicationController
    def show 
        @user = User.find(params[:id])
        render json: @user, status: :ok
    end
end

この時どのようなエラーが返却されるか考えてみます。

一番わかりやすいのはIDがUserになかった場合、ApplicationRecord#findにてActiveRecord::RecordNotFoundエラーが発生などでしょうか。

ただ上記の記述の場合、このエラーが発生した時にエラーハンドリングが記載されていないため、親クラスへ飛びます

簡単に言うと404の画面がレンダリングされるって感じです。

ただそれで本当にいいのでしょうか?


emailで取得する場合を考えるとわかりやすいかもしれません。

画面上でユーザーがemailを入力して、ちょっと間違えてしまった時に画面が切り替わってエラー画面が表示されるなんていうのはよくない形ですね。
一般的には、「入力したemailが間違ってます。」のようなメッセージを表示して戻されます。

この処理をControllerに加える場合どうするべきでしょうか?

class UserController < ApplicationController
    def show 
        @user = User.find_by!(email: params[:email])
        render json: @user, status: :ok
    rescue => e
        render json: {message: '入力したemailが間違ってます。'}, status: : not_found
    end
end


上記の場合だと入力されたemailに対し、Userが存在しない場合はエラーメッセージを返却する形になっています。
ただ、それ以外のエラーに対しても同じ内容を返しているのでよくない形ですね。

例えば、DataBase Serverが落ちた場合、Typoした場合、この後処理を追加したがそれが間違っていた場合などが挙げられます。
これはあり得ないことではないです。
※落としたトーストがバターを塗った面を下にして着地する確率は、絨毯の値段に比例する(マーフィーの法則)
※失敗する余地があるなら、失敗する(マーフィーの法則)
※ありえないなんてことはありえない(鋼の錬金術師 より グリード)

その場合でも「入力したemailが間違ってます。」とメッセージを表示するのはおかしいのではないでしょうか?


上記の場合であれば500エラーの画面を表示するなどが適切です。
そのため、Controllerは

class UserController < ApplicationController
    def show 
        @user = User.find_by!(email: params[:email])
        render json: @user, status: :ok
    rescue ActiveRecord::RecordNotFound => e
        render json: {message: e.to_s}, status: : not_found
    end
end


class ApplicationController < ActionController::Base
    rescue_from StandardError, with: :render_500

   def render_500(e)
       Rails.logger.error(e.message)
       render :error, status: :internal_server_error
   end
end

DRY原則や保守性の観点からエラーハンドリングは親クラスにて行うのが良いです。
さらに言うと、ApplicationControllerには各ロジックが加わるため、モジュールで分離する方が良いです。
そのため

class ApplicationController < ActionController::Base
    include ErrorHandler

end


module ErrorHandler
    extend ActiveSupport::Concern
    included do
        rescue_from StandardError, with: :render_500
    end

   def render_500(e)
       Rails.logger.error(e.message)
       render :error, status: :internal_server_error
   end
end


このerror_handler.rbは例として'lib/errors'などに置いておくのも良いですが、各ドメインで処理を分けている場合などならその都度改装を分けておくのも良いです。
参考資料ではself.includedを用いてますが、railsであればActiveSupport::Concernに加えるのも良いかと思います。
別途Sentryなどのエラー監視を行う場合はrender_xxxメソッドに追加して監視できます。

また特有のエラークラスを定義したい場合は config/exception.rb を作成し

 class UserNotFoundError < StandardError; end

のように独自定義し

class UserController < ApplicationController
    def show 
        @user = User.find_by(email: params[:email])
        raise UserNotFoundError.new('ユーザーが見つかりませんでした。') if @user.blank?
        render json: @user, status: :ok
    end
end

のように使うのも良いです。
参考としているものだと、親クラスを作成していますが、基本的にRuby,Railsで定義されているErrorクラスだけでも良い場合が多いためそこまでしなくてもいいかなと私は思います。