【Ruby】カスタムエラークラスを使用して、エラー処理をシンプルに実装する
Ruby でエラー処理を行う場合、元々 Ruby で定義されているエラークラスに加えて、以下のようにカスタムエラークラスを定義して使用できます。
class CustomError < StandardError; end
今回はカスタムエラークラスを使用したエラー処理の実装方法について説明します。
一例として Rails のアプリケーションにおいて、エラーの内容に応じたエラーレスポンスを生成し返すことを考えてみます。
以下のバージョンを前提としています。
- Ruby 3.3.7
- Rails 7.2.2
実装する要件
あるエンドポイントにおいて、以下の要件を満たすよう実装していきます。
- リクエストパラメーターの
params[:type]
が不正な場合のレスポンス- ステータスコード:
400
- レスポンスボディ:
{ "error": "Invalid type" }
- ステータスコード:
- リクエストパラメーターの
params[:password]
が不正な場合のレスポンス- ステータスコード:
400
- レスポンスボディ:
{ "error": "Incorrect password" }
- ステータスコード:
カスタムエラークラスを使用しない実装
まずはカスタムエラークラスを使わず、raise でエラーメッセージを引数に渡す方法で実装してみます。(verify_...
は引数が不正な場合 false
)
def index
raise 'Invalid type' unless verify_type(params[:type])
raise 'Incorrect password' unless verify_password(params[:password])
...
rescue RuntimeError => e
render status: 400, json: { error: e.message }
end
raise 'Invalid type'
は raise RuntimeError.new('Invalid type')
と同義[1]であるため rescue
の対象は RuntimeError
となります。
この実装でもシンプルで良さそうに見えますが、少なくとも以下のデメリットがあります。
-
#index
内の他の処理でRuntimeError
が発生した場合もrescue
対象としてしまう - ログや監視サービスなどで、エラークラスがすべて
RuntimeError
として記録されるため後から参照しづらい
一つ目のデメリットは begin ... rescue
で対象となる処理のみを囲うようにすれば多少は改善されるかもしれませんが、見栄えは悪くなってしまいそうです。
二つ目のデメリットについてもログや監視サービスの設定次第ではあるかもしれないですが、エラークラスからも何が起こったかある程度予測が付けばそれに越したことはありません。
カスタムエラークラスを使用して改善する
カスタムエラークラスを使用することで、上記のデメリットを改善できます。
カスタムエラークラスを定義し、エラーメッセージを引数として new したものを raise する方法で実装してみます。
class InvalidType < StandardError; end
class IncorrectPassword < StandardError; end
def index
raise InvalidType.new('Invalid type') unless verify_type(params[:type])
raise IncorrectPassword.new('Incorrect password') unless verify_password(params[:password])
...
rescue InvalidType, IncorrectPassword => e
render status: 400, json: { error: e.message }
end
これにより、別のエラーが誤って rescue されることはなくなり、ログや監視サービスにおける参照効率も改善しそうです。
複数のレスポンスパラメーターにも対応する
ただ、まだ気になるところはあります。
一つはエラークラスとエラーメッセージの内容が重複していることです。
せっかくエラーの内容をエラークラスで定義しているのに、raise する際にエラー内容を説明する同じようなメッセージも渡す必要があるのは若干冗長な気がします。
他にも、例えば最初の要件が変更になり、以下のように password
のエラーレスポンスに関してはステータスコードを 401
としたいとなったり、error_description
を追加して複数のパラメータを返す必要が生じた場合です。
- リクエストパラメーターの
params[:type]
が不正な場合のレスポンス- ステータスコード:
400
- レスポンスボディ:
{ "error": "Invalid type", + "error_description": "Type is invalid. Please set the valid type." }
- ステータスコード:
- リクエストパラメーターの
params[:password]
が不正な場合のレスポンス- ステータスコード:
→400
401
- レスポンスボディ:
{ "error": "Incorrect password", + "error_description": "Password is incorrect. Please set the correct password." }
- ステータスコード:
一個のエラーメッセージのみを引数で渡すこれまでの実装では、この要件に対応するのは難しそうです。
これらにはエラークラス側でエラーレスポンスに必要な情報を定義することで対応できます。
class BaseError < StandardError do
attr_reader :status_code, :error, :error_description
def initialize(status_code:, error:, error_description:)
@status_code = status_code
@error = error
@error_description = error_description
super
end
def to_response
{ status: status_code, json: { error:, error_description: } }
end
end
class InvalidType < BaseError do
def initialize
super(status_code: 400, error: 'Invalid type', error_description: 'Type is invalid. Please set the valid type.')
end
end
class IncorrectPassword < BaseError do
def initialize
super(status_code: 401, error: 'Incorrect password', error_description: 'Password is incorrect. Please set the correct password.')
end
end
def index
raise InvalidType unless verify_type(params[:type])
raise IncorrectPassword unless verify_password(params[:password])
...
rescue BaseError => e
render e.to_response
end
エラークラス側でエラーレスポンスに必要な情報を定義することで、大元となる index
はかなりシンプルな実装にすることができました。
今回の例ではエラーレスポンスの形が同じであるため、それぞれのエラークラスで BaseError
を継承することで、レスポンスの生成を行う to_response
は親の BaseError
に持っていくことができます。
raise
の引数はエラークラスにメッセージを付与して new したオブジェクトではなくエラークラスそのものの指定となり、冗長だった書き方も改善しています。
おわりに
Ruby でエラーに応じた処理を行う際にカスタムエラークラスを使用する方法について Rails のコードを例に説明しました。
エラー処理を実装する際の参考になれば幸いです。
-
正確には同スレッドの同じブロック内で最後に rescue された例外オブジェクトがある場合それが raise されますが、今回は無いものとします。
Kernel.#raise
↩︎
Discussion