Rubyのエラーハンドリングの基礎知識
rescueが必要な理由と基本的な使い方
プログラムの実行中に予期せぬエラーが発生することは避けられません。ファイルの読み込みに失敗したり、APIからの応答がタイムアウトしたり、データベースへの接続が切断されたりする可能性は常にあります。これらのエラーを適切に処理せずにプログラムが突然停止してしまうと、ユーザー体験が著しく損なわれ、データの整合性が失われる可能性もあります。
そこで重要になるのがrescue
を使用したエラーハンドリングです。以下のような利点があります:
- プログラムの堅牢性向上
- エラーが発生しても適切に処理を継続
- ユーザーへの適切なフィードバック提供
- デバッグの効率化
- エラーの発生箇所と原因の特定が容易
- ログによるエラー追跡が可能
- ユーザー体験の向上
- エラー時の代替処理の提供
- わかりやすいエラーメッセージの表示
基本的な使用例を見てみましょう:
# 基本的なrescueの使用例 begin # エラーが発生する可能性のある処理 result = potentially_dangerous_operation() rescue StandardError => e # エラーが発生した場合の処理 logger.error("エラーが発生しました: #{e.message}") # 代替の処理や適切なフィードバック notify_user("処理に失敗しました") end # メソッド定義内でのrescue def safe_operation # 危険な処理 result = risky_operation() rescue StandardError => e # エラー処理 handle_error(e) else # エラーが発生しなかった場合の処理 success_operation() ensure # 必ず実行される処理 cleanup_resources() end
Rubyに実装されている主要例外クラス
Rubyには様々な例外クラスが実装されており、階層構造を持っています。主要な例外クラスとその用途を理解することで、より適切なエラーハンドリングが可能になります。
例外クラスの階層構造
Exception ├── NoMemoryError # メモリ不足 ├── ScriptError │ ├── LoadError # ライブラリ読み込みエラー │ ├── NotImplementedError │ └── SyntaxError # 文法エラー └── StandardError # 通常のプログラムエラー ├── ArgumentError # 引数関連のエラー ├── IndexError # 配列のインデックスエラー ├── NameError # 未定義の変数/メソッド ├── NoMethodError # メソッド未定義 ├── RangeError # 範囲外の値 ├── RuntimeError # デフォルトのエラー └── TypeError # 型の不一致
主要な例外クラスの使用例
# ArgumentError def greet(name) raise ArgumentError, "名前を入力してください" if name.nil? puts "Hello, #{name}!" end # NoMethodError begin undefined_object.undefined_method rescue NoMethodError => e puts "未定義のメソッドが呼び出されました: #{e.message}" end # TypeError begin "string" + 1 # 文字列と数値の加算 rescue TypeError => e puts "型の不一致が発生しました: #{e.message}" end # カスタム例外の定義 class CustomError < StandardError def initialize(msg = "カスタムエラーが発生しました") super end end
これらの例外クラスを適切に使い分けることで、より具体的なエラー処理が可能になります。特に重要なのは:
StandardError
とそのサブクラスが最も一般的に使用されるrescue
節で例外クラスを指定しない場合はStandardError
が捕捉される- カスタム例外は通常
StandardError
を継承して作成する
エラーハンドリングの基本を押さえることで、より信頼性の高いプログラムを作成することができます。
Rubyのエラーハンドリングの基礎知識
rescueが必要な理由と基本的な使い方
プログラムの実行中に予期せぬエラーが発生することは避けられません。ファイルの読み込みに失敗したり、APIからの応答がタイムアウトしたり、データベースへの接続が切断されたりする可能性は常にあります。これらのエラーを適切に処理せずにプログラムが突然停止してしまうと、ユーザー体験が著しく損なわれ、データの整合性が失われる可能性もあります。
そこで重要になるのがrescue
を使用したエラーハンドリングです。以下のような利点があります:
- プログラムの堅牢性向上
- エラーが発生しても適切に処理を継続
- ユーザーへの適切なフィードバック提供
- デバッグの効率化
- エラーの発生箇所と原因の特定が容易
- ログによるエラー追跡が可能
- ユーザー体験の向上
- エラー時の代替処理の提供
- わかりやすいエラーメッセージの表示
基本的な使用例を見てみましょう:
# 基本的なrescueの使用例 begin # エラーが発生する可能性のある処理 result = potentially_dangerous_operation() rescue StandardError => e # エラーが発生した場合の処理 logger.error("エラーが発生しました: #{e.message}") # 代替の処理や適切なフィードバック notify_user("処理に失敗しました") end # メソッド定義内でのrescue def safe_operation # 危険な処理 result = risky_operation() rescue StandardError => e # エラー処理 handle_error(e) else # エラーが発生しなかった場合の処理 success_operation() ensure # 必ず実行される処理 cleanup_resources() end
Rubyに実装されている主要例外クラス
Rubyには様々な例外クラスが実装されており、階層構造を持っています。主要な例外クラスとその用途を理解することで、より適切なエラーハンドリングが可能になります。
例外クラスの階層構造
Exception ├── NoMemoryError # メモリ不足 ├── ScriptError │ ├── LoadError # ライブラリ読み込みエラー │ ├── NotImplementedError │ └── SyntaxError # 文法エラー └── StandardError # 通常のプログラムエラー ├── ArgumentError # 引数関連のエラー ├── IndexError # 配列のインデックスエラー ├── NameError # 未定義の変数/メソッド ├── NoMethodError # メソッド未定義 ├── RangeError # 範囲外の値 ├── RuntimeError # デフォルトのエラー └── TypeError # 型の不一致
主要な例外クラスの使用例
# ArgumentError def greet(name) raise ArgumentError, "名前を入力してください" if name.nil? puts "Hello, #{name}!" end # NoMethodError begin undefined_object.undefined_method rescue NoMethodError => e puts "未定義のメソッドが呼び出されました: #{e.message}" end # TypeError begin "string" + 1 # 文字列と数値の加算 rescue TypeError => e puts "型の不一致が発生しました: #{e.message}" end # カスタム例外の定義 class CustomError < StandardError def initialize(msg = "カスタムエラーが発生しました") super end end
これらの例外クラスを適切に使い分けることで、より具体的なエラー処理が可能になります。特に重要なのは:
StandardError
とそのサブクラスが最も一般的に使用されるrescue
節で例外クラスを指定しない場合はStandardError
が捕捉される- カスタム例外は通常
StandardError
を継承して作成する
エラーハンドリングの基本を押さえることで、より信頼性の高いプログラムを作成することができます。
rescueblock処理の効果的な実装パターン
基本的なrescue構文の書き方
rescueブロックの基本的な構文には、メソッド内での直接的な使用とbegin-rescue-endブロックの2つのパターンがあります。それぞれの特徴と使い分けを理解することが重要です。
# パターン1: メソッド内での直接的な使用 def process_data(data) # 危険な処理 process_dangerous_operation(data) rescue StandardError => e # エラー処理 log_error(e) raise # エラーを再送出する場合 end # パターン2: begin-rescue-endブロック begin # 危険な処理をブロックで囲む connection = DB.connect data = connection.query("SELECT * FROM users") rescue StandardError => e # エラー処理 log_error(e) ensure # リソースのクリーンアップ connection&.close end
どちらのパターンを選択するかは、以下の基準で判断します:
- メソッド内での直接的な使用
- メソッド全体がエラーハンドリングの対象の場合
- コードがシンプルで見通しが良い場合
- 単一の処理に対するエラー処理の場合
- begin-rescue-endブロック
- 特定のブロックのみをエラーハンドリングしたい場合
- 複数の処理を一つのエラーハンドリングでカバーする場合
- ensure句でリソースの解放が必要な場合
複数の例外を個別に扱う方法
実践的なアプリケーションでは、複数の種類の例外を個別に処理する必要があることがよくあります。Rubyでは、複数の例外を効率的に処理する方法が用意されています。
def complex_operation begin # データベース操作 result = db_operation() # API呼び出し api_result = api_call() # ファイル操作 file_result = file_operation() rescue ActiveRecord::RecordNotFound => e # レコードが見つからない場合の処理 log_error("データが見つかりません: #{e.message}") raise CustomNotFoundError, "要求されたリソースが見つかりません" rescue Net::HTTPError => e # API通信エラーの処理 log_error("API通信エラー: #{e.message}") notify_admin("APIエラーが発生しました") retry if should_retry? rescue Errno::ENOENT => e # ファイル関連エラーの処理 log_error("ファイルエラー: #{e.message}") create_empty_file retry rescue StandardError => e # その他の一般的なエラーの処理 log_error("予期せぬエラー: #{e.message}") notify_admin(e) end end
このパターンの利点:
- 例外の種類ごとに適切な処理が可能
- エラーメッセージをより具体的に設定可能
- 例外の種類に応じて異なるリカバリー戦略を実装可能
else句とensure句の適切な使用方法
else句とensure句を使用することで、より細かい制御が可能になります。これらの句の適切な使用方法を見ていきましょう。
def process_with_cleanup file = File.open("important.txt", "w") begin # 危険な処理 process_data(file) rescue IOError => e # ファイル操作エラーの処理 log_error("ファイル操作エラー: #{e.message}") false # 処理失敗を示す戻り値 else # エラーが発生しなかった場合の処理 log_success("処理が成功しました") true # 処理成功を示す戻り値 ensure # 必ず実行される処理(リソースのクリーンアップ) file.close if file end end # より実践的な例:トランザクション処理 def transaction_with_retry retry_count = 0 begin ActiveRecord::Base.transaction do # データベース処理 user.save! order.update!(status: 'completed') end rescue ActiveRecord::RecordInvalid => e # バリデーションエラーの処理 log_validation_error(e) raise rescue ActiveRecord::StaleObjectError => e # 楽観的ロックエラーの処理 retry_count += 1 if retry_count < 3 sleep(0.1 * retry_count) retry else raise end else # トランザクション成功時の処理 notify_success ensure # クリーンアップ処理 cleanup_temporary_data end end
else句とensure句の使用指針:
- else句の使用場合:
- エラーが発生しなかった場合の特別な処理が必要な時
- 正常系と異常系の処理を明確に分けたい時
- 処理の成功を明示的に記録したい時
- ensure句の使用場合:
- ファイルやデータベース接続のクローズ
- 一時ファイルの削除
- ロックの解放
- ログ出力の完了処理
これらのパターンを適切に組み合わせることで、より堅牢なエラーハンドリングを実現できます。
7つの実践的なエラーハンドリングテクニック
テクニック1:例外クラスの継承を活用した処理
例外クラスを継承して独自の例外クラスを作成することで、より細かいエラー制御が可能になります。
# 基底となる独自例外クラス class ApplicationError < StandardError attr_reader :code, :details def initialize(message = "An error occurred", code = nil, details = {}) @code = code @details = details super(message) end end # 具体的な例外クラス class ValidationError < ApplicationError def initialize(message = "Validation failed", details = {}) super(message, "VALIDATION_ERROR", details) end end class APIError < ApplicationError def initialize(message = "API request failed", details = {}) super(message, "API_ERROR", details) end end # 実践的な使用例 def process_user_data(user_params) begin validate_user(user_params) api_result = call_external_api(user_params) save_user_data(api_result) rescue ValidationError => e log_error("バリデーションエラー", e.code, e.details) raise rescue APIError => e log_error("API呼び出しエラー", e.code, e.details) notify_admin(e) false end end
テクニック2:retry機能を使用した自動リカバリー
一時的な障害に対して、retryを使用して自動的にリカバリーを試みることができます。
class APIClient MAX_RETRIES = 3 RETRY_WAIT = 1.0 def self.fetch_data(endpoint) retries = 0 begin response = HTTP.timeout(5).get(endpoint) handle_response(response) rescue HTTP::TimeoutError, HTTP::ConnectionError => e retries += 1 if retries <= MAX_RETRIES wait_time = RETRY_WAIT * (2 ** (retries - 1)) # 指数バックオフ logger.warn("API呼び出しに失敗しました。#{wait_time}秒後にリトライします。(#{retries}/#{MAX_RETRIES})") sleep(wait_time) retry else logger.error("APIリトライ上限に達しました: #{e.message}") raise APIError.new("API呼び出しが#{MAX_RETRIES}回失敗しました", details: { endpoint: endpoint }) end end end private def self.handle_response(response) case response.code when 200..299 JSON.parse(response.body.to_s) when 404 raise NotFoundError.new("リソースが見つかりません") when 500..599 raise ServerError.new("サーバーエラーが発生しました") else raise APIError.new("予期せぬレスポンスコード: #{response.code}") end end end
テクニック3:begin-rescueのスコープ最適化
エラーハンドリングのスコープを最適化することで、より正確なエラー処理が可能になります。
class DocumentProcessor def process_document(document_path) # ファイルの存在チェックは個別に行う unless File.exist?(document_path) raise ArgumentError, "ファイルが存在しません: #{document_path}" end begin # ファイル読み込み処理のみをrescueで囲む content = File.read(document_path) rescue SystemCallError => e logger.error("ファイル読み込みエラー: #{e.message}") raise IOError, "ファイルを読み込めません: #{e.message}" end begin # パース処理は別のrescueブロックで処理 parsed_data = JSON.parse(content) rescue JSON::ParserError => e logger.error("JSONパースエラー: #{e.message}") raise ValidationError, "不正なJSONフォーマット" end # 処理結果を返す processed_result = process_data(parsed_data) { status: :success, data: processed_result } end end
テクニック4:例外情報の効果的なログ記録
デバッグや障害分析に役立つ情報を適切にログに記録することが重要です。
module ErrorLogger class << self def log_error(error, context = {}) error_info = { error_class: error.class.name, message: error.message, backtrace: error.backtrace&.first(5), timestamp: Time.current, environment: Rails.env, context: context } # エラー情報の構造化 case error when ActiveRecord::RecordInvalid log_validation_error(error, error_info) when ActionController::ParameterMissing log_parameter_error(error, error_info) else log_general_error(error, error_info) end end private def log_validation_error(error, info) info[:validation_errors] = error.record.errors.messages Rails.logger.warn(info.to_json) end def log_parameter_error(error, info) info[:parameters] = error.param Rails.logger.warn(info.to_json) end def log_general_error(error, info) Rails.logger.error(info.to_json) end end end
テクニック5:カスタム例外クラスの設計と活用
ドメイン固有の例外を定義することで、より明確なエラー処理が可能になります。
module PaymentSystem # 支払い関連の基底例外クラス class PaymentError < StandardError attr_reader :payment_id, :error_code def initialize(message, payment_id: nil, error_code: nil) @payment_id = payment_id @error_code = error_code super(message) end end # 具体的な例外クラス class InsufficientFundsError < PaymentError def initialize(payment_id:, amount:, balance:) super( "残高不足です(必要額: #{amount}円、残高: #{balance}円)", payment_id: payment_id, error_code: 'INSUFFICIENT_FUNDS' ) end end class PaymentProcessingError < PaymentError def initialize(payment_id:, provider_message:) super( "決済処理に失敗しました: #{provider_message}", payment_id: payment_id, error_code: 'PROCESSING_ERROR' ) end end # 実装例 class PaymentProcessor def process_payment(payment) validate_payment(payment) process_with_provider(payment) rescue PaymentError => e handle_payment_error(e) end private def validate_payment(payment) if payment.amount > payment.user.balance raise InsufficientFundsError.new( payment_id: payment.id, amount: payment.amount, balance: payment.user.balance ) end end end end
テクニック6:rescue修飾子を使ったシンプルな実装
単純なエラー処理の場合、rescue修飾子を使用してコードをシンプルに保つことができます。
class CacheManager def fetch_cached_data(key) # キャッシュから値を取得、失敗時はnilを返す Rails.cache.read(key) rescue nil end def safe_parse_json(string) # JSON解析、失敗時は空のハッシュを返す JSON.parse(string) rescue {} end def fetch_user_preference(user_id) # データベースから設定を取得、失敗時はデフォルト値を返す UserPreference.find_by(user_id: user_id)&.settings rescue DEFAULT_SETTINGS end end
テクニック7:raise/rescueによる例外の再送信
エラー情報を保持しながら、より適切な例外に変換して再送信することができます。
class APIGateway class << self def fetch_user_data(user_id) response = make_api_request("/users/#{user_id}") process_response(response) rescue RestClient::NotFound => e # 404エラーを適切な独自例外に変換 raise UserNotFoundError.new( "ユーザーが見つかりません: #{user_id}", original_error: e ) rescue RestClient::Unauthorized => e # 認証エラーを記録して再送信 logger.error("API認証エラー: #{e.message}") raise AuthenticationError.new( "API認証に失敗しました", original_error: e ) rescue RestClient::Exception => e # その他のAPIエラーを汎用エラーとして扱う handle_api_error(e) end private def handle_api_error(error) logger.error("APIエラー: #{error.message}") logger.error("レスポンス: #{error.response}") raise APIError.new( "APIリクエストに失敗しました", status: error.response.code, body: error.response.body, original_error: error ) end end end # カスタムエラークラス class APIError < StandardError attr_reader :status, :body, :original_error def initialize(message, status: nil, body: nil, original_error: nil) @status = status @body = body @original_error = original_error super(message) end end
これらのテクニックを状況に応じて適切に組み合わせることで、より堅牢なエラーハンドリングを実現できます。
エラーハンドリングのベストプラクティス
無駄な例外処理を避けるための設計指針
効率的なエラーハンドリングを実現するには、適切な設計指針に従うことが重要です。以下に、Rubyでの例外処理における重要な設計原則を示します。
1. 例外の適切な粒度設定
# 悪い例:大きすぎる粒度 def process_order(order) begin # 全ての処理を1つのbegin-rescue句で囲む validate_order(order) process_payment(order) update_inventory(order) send_confirmation(order) rescue StandardError => e # エラーの種類が特定できない logger.error(e) raise end end # 良い例:適切な粒度でエラーハンドリング class OrderProcessor def process_order(order) validate_order(order) process_payment(order) update_inventory(order) send_confirmation(order) rescue ValidationError => e handle_validation_error(e, order) rescue PaymentError => e handle_payment_error(e, order) rescue InventoryError => e handle_inventory_error(e, order) rescue NotificationError => e handle_notification_error(e, order) end private def handle_validation_error(error, order) logger.error("注文バリデーションエラー: #{error.message}", order_id: order.id) raise OrderError.new("注文の検証に失敗しました", cause: error) end # 他のエラーハンドラーメソッド... end
2. 例外の適切な階層構造
# 例外クラスの階層構造の例 module ECommerce # 基底例外クラス class Error < StandardError; end # 機能別の基底例外クラス class OrderError < Error; end class PaymentError < Error; end class InventoryError < Error; end # より具体的な例外クラス module Order class ValidationError < OrderError; end class ProcessingError < OrderError; end end module Payment class InsufficientFundsError < PaymentError; end class CardDeclinedError < PaymentError; end end end # 使用例 def process_payment validate_payment_info rescue ECommerce::Payment::InsufficientFundsError => e notify_user_insufficient_funds(e) rescue ECommerce::Payment::CardDeclinedError => e request_alternative_payment_method(e) end
テストの可能性を考慮したrescueの実装方法
エラーハンドリングのテスト容易性を確保するための実装パターンとテスト方法を見ていきましょう。
1. テスト可能なエラーハンドリング設計
# テスト容易性を考慮したサービスクラス class PaymentService class << self def process_payment(payment_params) new.process_payment(payment_params) end end def process_payment(payment_params) validate_params(payment_params) charge = create_charge(payment_params) record_transaction(charge) notify_user(charge) { success: true, charge_id: charge.id } rescue ValidationError => e handle_validation_error(e) rescue PaymentProcessingError => e handle_processing_error(e) end private def validate_params(params) validator = PaymentParamsValidator.new(params) raise ValidationError, validator.errors unless validator.valid? end def create_charge(params) payment_gateway.create_charge(params) rescue PaymentGateway::Error => e raise PaymentProcessingError, e.message end def handle_validation_error(error) log_error(error) { success: false, error: error.message, type: :validation_error } end def handle_processing_error(error) log_error(error) notify_admin(error) { success: false, error: error.message, type: :processing_error } end def payment_gateway @payment_gateway ||= PaymentGateway.new end end
2. エラーハンドリングのテストケース
RSpec.describe PaymentService do let(:service) { described_class.new } let(:valid_params) do { amount: 1000, currency: 'JPY', card_token: 'tok_valid' } end describe '#process_payment' do context '正常系' do it '支払い処理が成功する' do result = service.process_payment(valid_params) expect(result[:success]).to be true expect(result[:charge_id]).to be_present end end context 'バリデーションエラーの場合' do let(:invalid_params) { valid_params.merge(amount: -1) } it '適切なエラーレスポンスを返す' do result = service.process_payment(invalid_params) expect(result[:success]).to be false expect(result[:type]).to eq(:validation_error) expect(result[:error]).to include('金額が不正です') end end context '決済処理エラーの場合' do before do allow(service.send(:payment_gateway)) .to receive(:create_charge) .and_raise(PaymentGateway::Error.new('カード決済に失敗しました')) end it '適切なエラーレスポンスを返す' do result = service.process_payment(valid_params) expect(result[:success]).to be false expect(result[:type]).to eq(:processing_error) expect(result[:error]).to include('カード決済に失敗しました') end it '管理者に通知する' do expect(service) .to receive(:notify_admin) .with(instance_of(PaymentProcessingError)) service.process_payment(valid_params) end end end end
3. モック/スタブを活用したテスト
RSpec.describe OrderProcessor do let(:processor) { described_class.new } let(:order) { build(:order) } # 外部サービスのモック let(:payment_service) { instance_double('PaymentService') } let(:inventory_service) { instance_double('InventoryService') } let(:notification_service) { instance_double('NotificationService') } before do allow(processor).to receive(:payment_service).and_return(payment_service) allow(processor).to receive(:inventory_service).and_return(inventory_service) allow(processor).to receive(:notification_service).and_return(notification_service) end describe '#process_order' do context '支払い処理でエラーが発生した場合' do before do allow(payment_service) .to receive(:process) .and_raise(PaymentError.new('支払い処理に失敗しました')) end it 'エラーを適切にハンドリングする' do expect { processor.process_order(order) } .to change { order.status } .to('payment_failed') end it 'ログを記録する' do expect(Rails.logger) .to receive(:error) .with(/支払い処理に失敗しました/) processor.process_order(order) end end end end
これらのベストプラクティスと実装パターンを活用することで、保守性が高く、テストが容易なエラーハンドリングを実現できます。
実際のプロジェクトでよくあるエラーとその対処法
データベース操作時の例外処理パターン
データベース操作では、様々な例外が発生する可能性があります。以下に主要なエラーパターンと対処法を示します。
class DatabaseOperationService class << self def safe_transaction ActiveRecord::Base.transaction do yield rescue ActiveRecord::RecordNotFound => e handle_not_found_error(e) rescue ActiveRecord::RecordInvalid => e handle_validation_error(e) rescue ActiveRecord::StaleObjectError => e handle_optimistic_lock_error(e) rescue ActiveRecord::StatementInvalid => e handle_sql_error(e) rescue ActiveRecord::ConnectionTimeoutError => e handle_connection_timeout(e) end end private def handle_not_found_error(error) Rails.logger.error("データが見つかりません: #{error.message}") raise EntityNotFoundError.new("要求されたリソースが見つかりません", original_error: error) end def handle_validation_error(error) Rails.logger.error("バリデーションエラー: #{error.record.errors.full_messages}") raise ValidationError.new("データの検証に失敗しました", details: error.record.errors) end def handle_optimistic_lock_error(error) Rails.logger.error("楽観的ロックエラー: #{error.message}") raise ConcurrencyError.new("データが他のユーザーによって更新されています") end def handle_sql_error(error) Rails.logger.error("SQLエラー: #{error.message}") if error.message.include?('duplicate key value') raise DuplicateRecordError.new("既に同じデータが存在します") else raise DatabaseError.new("データベース操作に失敗しました") end end def handle_connection_timeout(error) Rails.logger.error("DB接続タイムアウト: #{error.message}") raise DatabaseConnectionError.new("データベースとの接続がタイムアウトしました") end end end # 使用例 class OrderProcessor def process_bulk_orders(orders) DatabaseOperationService.safe_transaction do orders.each do |order| order.with_lock do process_single_order(order) end rescue OrderError => e handle_order_error(order, e) end end end private def process_single_order(order) update_inventory(order) update_order_status(order) create_shipment(order) end def handle_order_error(order, error) order.update_columns( status: :processing_failed, error_message: error.message ) notify_admin_of_failure(order, error) end end
外部APIと連携時のエラーハンドリング
外部APIとの連携では、ネットワークエラーやタイムアウトなど、様々な例外が発生する可能性があります。
class APIClient class << self def with_retry(max_retries: 3, base_wait_time: 1) retries = 0 begin yield rescue Net::OpenTimeout, Net::ReadTimeout => e handle_timeout_error(e, retries, max_retries, base_wait_time) rescue Faraday::ConnectionFailed => e handle_connection_error(e, retries, max_retries, base_wait_time) rescue Faraday::ClientError => e handle_client_error(e) rescue Faraday::ServerError => e handle_server_error(e) end end private def handle_timeout_error(error, retries, max_retries, base_wait_time) if retries < max_retries wait_time = calculate_backoff(base_wait_time, retries) log_retry_attempt(error, retries, wait_time) sleep(wait_time) retries += 1 retry else raise APITimeoutError.new("APIリクエストがタイムアウトしました", original_error: error) end end def handle_connection_error(error, retries, max_retries, base_wait_time) if retries < max_retries wait_time = calculate_backoff(base_wait_time, retries) log_retry_attempt(error, retries, wait_time) sleep(wait_time) retries += 1 retry else raise APIConnectionError.new("API接続に失敗しました", original_error: error) end end def handle_client_error(error) case error.response[:status] when 401 raise APIAuthenticationError.new("API認証に失敗しました") when 403 raise APIAuthorizationError.new("APIアクセスが拒否されました") when 404 raise APIResourceNotFoundError.new("APIリソースが見つかりません") else raise APIClientError.new("APIクライアントエラーが発生しました", status: error.response[:status]) end end def handle_server_error(error) raise APIServerError.new("APIサーバーエラーが発生しました", status: error.response[:status]) end def calculate_backoff(base_time, retry_count) base_time * (2 ** retry_count) + rand(0.1..0.5) end def log_retry_attempt(error, retries, wait_time) Rails.logger.warn("APIリトライ実行 (#{retries + 1}回目): #{error.message}. #{wait_time}秒後に再試行") end end end # 実装例 class PaymentGateway def process_payment(payment) APIClient.with_retry do response = api_client.post('/payments', payment.to_json) handle_payment_response(response) end rescue APIError => e handle_payment_error(e, payment) end private def handle_payment_response(response) case response.status when 200..299 process_successful_payment(response.body) else raise PaymentProcessingError.new("支払い処理に失敗しました", response: response) end end def handle_payment_error(error, payment) payment.update(status: :failed, error_message: error.message) notify_payment_failure(payment, error) raise PaymentError.new("支払い処理中にエラーが発生しました", original_error: error) end end
並行処理における例外処理の注意点
並行処理を行う際は、スレッドやプロセス間でのエラーハンドリングに特に注意が必要です。
class BatchProcessor class << self def process_in_parallel(items, max_threads: 5) results = { successful: [], failed: [] } mutex = Mutex.new error_queue = Queue.new ThreadsWait.all(*items.each_slice(items.size / max_threads).map { |batch| Thread.new do begin process_batch(batch, mutex, results) rescue StandardError => e error_queue << e end end }) handle_thread_errors(error_queue) results end private def process_batch(items, mutex, results) items.each do |item| begin processed_item = process_single_item(item) mutex.synchronize { results[:successful] << processed_item } rescue StandardError => e mutex.synchronize { results[:failed] << { item: item, error: e.message, backtrace: e.backtrace&.first(5) } } log_processing_error(item, e) end end end def process_single_item(item) # 具体的な処理をここに実装 raise NotImplementedError end def handle_thread_errors(error_queue) errors = [] until error_queue.empty? errors << error_queue.pop end return if errors.empty? raise BatchProcessingError.new( "バッチ処理中に#{errors.size}件のエラーが発生しました", errors: errors ) end def log_processing_error(item, error) Rails.logger.error( "項目処理エラー: #{error.message}\n" \ "項目: #{item.inspect}\n" \ "バックトレース: #{error.backtrace&.first(5)&.join("\n")}" ) end end end # 使用例 class ImageProcessor < BatchProcessor class << self private def process_single_item(image) validate_image(image) resized_image = resize_image(image) upload_to_storage(resized_image) update_image_status(image) rescue ImageValidationError => e raise ImageProcessingError.new("画像の検証に失敗しました", image: image, cause: e) rescue ImageResizeError => e raise ImageProcessingError.new("画像のリサイズに失敗しました", image: image, cause: e) rescue StorageError => e raise ImageProcessingError.new("画像のアップロードに失敗しました", image: image, cause: e) end end end
これらのパターンと注意点を適切に組み合わせることで、より堅牢なエラーハンドリングを実現できます。