🟧

【Ruby】カスタムエラークラスを使用して、エラー処理をシンプルに実装する

2025/02/18に公開

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] が不正な場合のレスポンス
    • ステータスコード: 400401
    • レスポンスボディ:
    { 
      "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 のコードを例に説明しました。
エラー処理を実装する際の参考になれば幸いです。

脚注
  1. 正確には同スレッドの同じブロック内で最後に rescue された例外オブジェクトがある場合それが raise されますが、今回は無いものとします。 Kernel.#raise ↩︎

SocialPLUS Tech Blog

Discussion