[Rails]カスタム例外で華麗な例外処理を実現したい！

# はじめに
最近、業務の中でカスタム例外を扱う機会がありましたので、活用法やメリットなどを自分の備忘録がてら記事にしようと思います。

# カスタム例外って何よ？何のために使うの？
カスタム例外とは、**開発者が独自に定義する例外クラス**です。通常、StandardError や ActiveRecord::RecordInvalid といった標準の例外クラスを利用することができますが、特定のビジネスロジックやドメインに応じたエラー処理を行う際に、カスタム例外を使うことでエラーメッセージがわかりやすくなり、デバッグやトラブルシューティングがしやすくなります。

カスタム例外を使うメリットは以下の通りです。

**特定のエラーを識別しやすくする**
通常のエラーと区別することで、エラー発生時により的確な対応が可能です。

**エラーメッセージやログをカスタマイズできる**
エラーの内容に応じて、より詳細で有用なメッセージを提供できます。

**例外処理を共通化して、コードの重複を排除できる**
共通の処理でエラー対応を統一し、メンテナンス性を向上させます。

### 基本的な使い方
カスタム例外は、Rubyの標準クラスStandardErrorを継承して作成することが多いかなと思います。以下は、カスタム例外の定義例です。
```ruby
class CustomError < StandardError; end

class InvalidUsernameError < CustomError
  def initialize(message = 'Invalid username')
    super(message)
  end
end
```
このように、StandardErrorからカスタムクラスを継承し、特定のエラーメッセージや処理を定義できます。

# カスタム例外を使わない実装例
まずは、カスタム例外を使わない場合のコードを見てみましょう。ユーザー登録機能を例に挙げます。

```Ruby
class UsersController < ApplicationController
  def create
    username = params[:username]
    email = params[:email]

    # ユーザー名が無効な場合
    unless valid_username?(username)
      return render_error('Invalid username', :unprocessable_entity)
    end

    # メールアドレスが無効な場合
    unless valid_email?(email)
      return render_error('Invalid email address', :unprocessable_entity)
    end

    begin
      # ユーザー作成処理
      User.create!(username: username, email: email)
      render json: { message: 'User created successfully' }, status: :ok
    rescue ActiveRecord::RecordInvalid
      render_error('Failed to create user', :unprocessable_entity)
    rescue StandardError
      render_error('An unexpected error occurred', :internal_server_error)
    end
  end

  private

  def valid_username?(username)
    # バリデーション処理
  end

  def valid_email?(email)
    # バリデーション処理
  end

  def render_error(error_message, status)
    render json: { error_message: error_message }, status: status
  end
end

```

この実装にはいくつかの問題があります。

**特定のエラーの識別が不十分**
標準の例外クラスを使用しているため、エラーの特定が困難です。たとえば、StandardError や ActiveRecord::RecordInvalid では、エラーの具体的な内容が不明瞭で、発生原因を迅速に特定するのが難しくなります。

**例外処理が分散している**
各例外処理が個別に実装されており、メンテナンスが難しい構造になっています。新たなエラーハンドリングを追加する際に、複数箇所に修正が必要になり、コードの複雑さが増します。

**共通化できていないエラー処理**
メールアドレスやユーザー名のバリデーションエラーは非常に似た処理を行っていますが、それぞれが別々に処理されているため、コードの冗長性が高まっています。同じような処理が複数箇所で記述されていると、修正や追加が煩雑になり、バグが発生しやすくなります。


# カスタム例外を使って改善💪
次に、カスタム例外を使った実装を見てみましょう〜

```Ruby
class UsersController < ApplicationController
  # カスタム例外の定義
  class RegistrationError < StandardError; end

  class InvalidUsernameError < RegistrationError
    def initialize(message = 'Invalid username')
      super(message)
    end
  end

  class InvalidEmailError < RegistrationError
    def initialize(message = 'Invalid email format')
      super(message)
    end
  end

  class CreationFailedError < RegistrationError
    def initialize(message = 'Failed to create user')
      super(message)
    end
  end

  # カスタム例外の例外処理を一元化
  rescue_from RegistrationError, with: :handle_registration_error

  def create
    username = params[:username]
    email = params[:email]

    # ユーザー名の検証
    raise InvalidUsernameError unless valid_username?(username)

    # メールアドレスの検証
    raise InvalidEmailError unless valid_email?(email)

    # ユーザー作成処理
    User.create!(username: username, email: email)
    render json: { message: 'User created successfully' }, status: :ok
  rescue ActiveRecord::RecordInvalid
    raise CreationFailedError
  end

  private

  def valid_username?(username)
    # バリデーション処理
  end

  def valid_email?(email)
    # バリデーション処理
  end

  # カスタム例外の共通処理
  def handle_registration_error(exception)
    logger.error("Registration failed: #{exception.message}")

    case exception
    when InvalidUsernameError, InvalidEmailError, CreationFailedError
      render_error(exception.message, :unprocessable_entity)
    else
      render_error('An unexpected error occurred', :internal_server_error)
    end
  end

  def render_error(error_message, status)
    render json: { error_message: error_message }, status: status
  end
end

```

### 改善点の詳細
**特定のエラーの識別が容易**
RegistrationError を継承したカスタム例外を利用することで、発生したエラーがどの部分で、どのような理由で起きたのかを具体的に識別しやすくなっています。たとえば、InvalidUsernameError と InvalidEmailError というように、特定の問題に応じたエラーが定義されているため、発生源を迅速に特定し、適切な対応が可能です。

**例外処理の一元化**
rescue_from を使うことで、すべてのカスタム例外を一箇所で処理できます。これにより、エラーハンドリングのロジックが集中し、コード全体の可読性と保守性が向上します。新しいエラータイプが追加される際も、共通の処理を利用することで効率的に対応できます。

**エラー処理の共通化**
バリデーションや作成エラーの処理がカスタム例外に集約されることで、重複するコードが減り、よりシンプルでわかりやすい実装が可能になります。たとえば、ユーザー名やメールアドレスのバリデーションエラーは、それぞれのカスタム例外を投げるだけで済み、エラー処理の重複を避けることができます。


# まとめ
今回の例では、Railsアプリケーションにおけるユーザー登録機能を例にして、カスタム例外を使うことでどのようにエラー処理が改善されるかを示しました。特定のエラーをより明確に区別し、適切に処理できるようにすることで、コードの可読性や保守性が大幅に向上した感じがしますね！

# 参考サイト
https://product.st.inc/entry/2020/12/04/110528