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
これらのパターンと注意点を適切に組み合わせることで、より堅牢なエラーハンドリングを実現できます。