💎
【Ruby 9日目】基本文法 - 例外処理
はじめに
Rubyの例外処理について、Ruby 3.4の仕様に基づいて詳しく解説します。例外処理は、エラーに対処し、プログラムの堅牢性を高めるための重要な機能です。
この記事では、基本的な概念から実践的な使い方まで、具体的なコード例を交えて説明します。
基本概念
例外処理の主な特徴:
- 例外(Exception): エラーや異常な状況を表すオブジェクト
- begin-rescue: 例外を捕捉して処理する
- raise: 例外を発生させる
- ensure: 必ず実行される処理
Rubyでは例外処理を適切に使用することで、堅牢で保守性の高いコードを書くことができます。
基本的な使い方
begin-rescue-end
# 基本的な例外処理
begin
# 例外が発生する可能性のあるコード
result = 10 / 0
rescue
# 例外が発生した時の処理
puts "エラーが発生しました"
end
# 例外オブジェクトを取得
begin
result = 10 / 0
rescue => e
puts "エラー: #{e.message}"
puts "クラス: #{e.class}"
end
# => エラー: divided by 0
# => クラス: ZeroDivisionError
特定の例外を捕捉
# 複数の例外を個別に処理
begin
file = File.open("存在しないファイル.txt")
content = file.read
rescue Errno::ENOENT => e
puts "ファイルが見つかりません: #{e.message}"
rescue IOError => e
puts "I/Oエラー: #{e.message}"
rescue => e
puts "その他のエラー: #{e.message}"
end
# 複数の例外を同時に捕捉
begin
# 処理
rescue Errno::ENOENT, Errno::EACCES => e
puts "ファイルアクセスエラー: #{e.message}"
end
else節
# 例外が発生しなかった場合の処理
begin
result = 10 / 2
rescue ZeroDivisionError
puts "0で割ることはできません"
else
puts "計算成功: #{result}" # 例外が発生しなかった場合のみ実行
end
ensure節
# 必ず実行される処理
def read_file(filename)
file = nil
begin
file = File.open(filename)
content = file.read
return content
rescue Errno::ENOENT => e
puts "ファイルが見つかりません: #{e.message}"
return nil
ensure
# 例外の有無に関わらず必ず実行される
file&.close
puts "ファイルをクローズしました"
end
end
メソッド定義内での省略形
# beginを省略できる
def risky_operation
result = 10 / 0
rescue ZeroDivisionError
puts "0で割ることはできません"
return nil
end
risky_operation # => 0で割ることはできません
retry(再試行)
def fetch_data_with_retry
retries = 0
max_retries = 3
begin
puts "データ取得中... (試行 #{retries + 1})"
# ランダムに失敗をシミュレート
raise "接続エラー" if rand < 0.7
puts "データ取得成功!"
return "データ"
rescue => e
retries += 1
if retries < max_retries
puts "エラー: #{e.message}. 再試行します..."
sleep 1
retry # beginブロックの先頭に戻る
else
puts "最大試行回数に達しました"
raise # 例外を再発生
end
end
end
# fetch_data_with_retry
raise(例外の発生)
# 例外を発生させる
def validate_age(age)
raise ArgumentError, "年齢は0以上である必要があります" if age < 0
raise ArgumentError, "年齢は150以下である必要があります" if age > 150
puts "年齢: #{age}"
end
begin
validate_age(-5)
rescue ArgumentError => e
puts "バリデーションエラー: #{e.message}"
end
# 例外クラスを指定して発生
raise StandardError.new("カスタムエラーメッセージ")
# 省略形
raise "エラーが発生しました" # RuntimeErrorが発生
よくあるユースケース
ケース1: ファイル操作
class FileProcessor
def process(filename)
begin
File.open(filename, "r") do |file|
content = file.read
process_content(content)
end
rescue Errno::ENOENT
puts "エラー: ファイル '#{filename}' が見つかりません"
false
rescue Errno::EACCES
puts "エラー: ファイル '#{filename}' へのアクセスが拒否されました"
false
rescue => e
puts "予期しないエラー: #{e.class} - #{e.message}"
false
else
puts "ファイル処理が正常に完了しました"
true
ensure
puts "処理を終了します"
end
end
private
def process_content(content)
# コンテンツ処理
puts "コンテンツを処理中..."
end
end
# 使用例
processor = FileProcessor.new
processor.process("data.txt")
ケース2: カスタム例外クラス
# カスタム例外の定義
class ValidationError < StandardError
attr_reader :field, :value
def initialize(field, value, message)
@field = field
@value = value
super("#{field}のバリデーションエラー: #{message} (値: #{value})")
end
end
class NotFoundError < StandardError
end
class User
attr_accessor :name, :email, :age
def initialize(name:, email:, age:)
validate_name(name)
validate_email(email)
validate_age(age)
@name = name
@email = email
@age = age
end
private
def validate_name(name)
if name.nil? || name.empty?
raise ValidationError.new(:name, name, "名前は必須です")
end
if name.length < 2
raise ValidationError.new(:name, name, "名前は2文字以上である必要があります")
end
end
def validate_email(email)
unless email.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
raise ValidationError.new(:email, email, "メールアドレスの形式が正しくありません")
end
end
def validate_age(age)
unless age.is_a?(Integer) && age >= 0 && age <= 150
raise ValidationError.new(:age, age, "年齢は0〜150の整数である必要があります")
end
end
end
# 使用例
begin
user = User.new(name: "A", email: "invalid", age: 30)
rescue ValidationError => e
puts "バリデーションエラーが発生しました:"
puts " フィールド: #{e.field}"
puts " 値: #{e.value}"
puts " メッセージ: #{e.message}"
end
ケース3: API呼び出しとエラーハンドリング
class ApiClient
class ApiError < StandardError; end
class ConnectionError < ApiError; end
class AuthenticationError < ApiError; end
class NotFoundError < ApiError; end
class ServerError < ApiError; end
def fetch_user(user_id)
retries = 0
max_retries = 3
begin
response = make_request("/users/#{user_id}")
handle_response(response)
rescue ConnectionError => e
retries += 1
if retries < max_retries
puts "接続エラー。#{retries}秒後に再試行します..."
sleep retries
retry
else
puts "接続エラー: 最大試行回数に達しました"
raise
end
rescue AuthenticationError => e
puts "認証エラー: #{e.message}"
nil
rescue NotFoundError => e
puts "ユーザーが見つかりません: ID #{user_id}"
nil
rescue ServerError => e
puts "サーバーエラー: #{e.message}"
nil
end
end
private
def make_request(path)
# 仮想的なHTTPリクエスト
status_code = [200, 401, 404, 500, 503].sample
{
status: status_code,
body: { id: 1, name: "Alice" }
}
end
def handle_response(response)
case response[:status]
when 200
response[:body]
when 401
raise AuthenticationError, "認証に失敗しました"
when 404
raise NotFoundError, "リソースが見つかりません"
when 500..599
raise ServerError, "サーバーエラーが発生しました (#{response[:status]})"
else
raise ApiError, "予期しないステータスコード: #{response[:status]}"
end
end
end
# 使用例
client = ApiClient.new
user = client.fetch_user(123)
puts user if user
ケース4: トランザクション処理
class BankAccount
attr_reader :balance
def initialize(balance)
@balance = balance
end
def withdraw(amount)
raise ArgumentError, "金額は正の数である必要があります" if amount <= 0
raise "残高不足です" if amount > @balance
@balance -= amount
amount
end
def deposit(amount)
raise ArgumentError, "金額は正の数である必要があります" if amount <= 0
@balance += amount
end
end
class TransferService
class TransferError < StandardError; end
def transfer(from_account, to_account, amount)
# トランザクション開始
original_from = from_account.balance
original_to = to_account.balance
begin
# 出金
from_account.withdraw(amount)
# エラーをシミュレート(ランダム)
raise TransferError, "送金処理中にエラーが発生しました" if rand < 0.3
# 入金
to_account.deposit(amount)
puts "送金成功: #{amount}円"
true
rescue => e
# ロールバック
puts "エラーが発生しました。トランザクションをロールバックします"
puts "エラー内容: #{e.message}"
# 元に戻す(簡易的な実装)
from_account.instance_variable_set(:@balance, original_from)
to_account.instance_variable_set(:@balance, original_to)
false
end
end
end
# 使用例
account_a = BankAccount.new(10000)
account_b = BankAccount.new(5000)
service = TransferService.new
puts "送金前:"
puts "口座A: #{account_a.balance}円"
puts "口座B: #{account_b.balance}円"
service.transfer(account_a, account_b, 3000)
puts "\n送金後:"
puts "口座A: #{account_a.balance}円"
puts "口座B: #{account_b.balance}円"
例外クラスの階層
# Rubyの例外クラス階層(主要なもの)
# Exception
# ├── StandardError(通常捕捉すべき例外)
# │ ├── ArgumentError
# │ ├── IOError
# │ ├── RuntimeError
# │ ├── TypeError
# │ ├── NameError
# │ │ └── NoMethodError
# │ └── ZeroDivisionError
# ├── ScriptError
# │ ├── SyntaxError
# │ └── LoadError
# ├── SignalException
# │ └── Interrupt
# └── SystemExit
# StandardErrorを継承する(推奨)
class MyError < StandardError
end
# 使用例:特定の例外のみ捕捉
begin
raise MyError, "カスタムエラー"
rescue StandardError => e
puts "StandardError系の例外: #{e.message}"
rescue Exception => e
# これは通常やるべきではない!
# SystemExitやInterruptまで捕捉してしまう
puts "すべての例外: #{e.message}"
end
注意点とベストプラクティス
注意点
- 例外を握りつぶさない
# BAD: 例外を無視
begin
risky_operation
rescue
# 何もしない(危険!)
end
# GOOD: 少なくともログを出力
begin
risky_operation
rescue => e
logger.error("エラーが発生しました: #{e.message}")
logger.error(e.backtrace.join("\n"))
raise # 必要に応じて再発生
end
- Exceptionを直接捕捉しない
# BAD: SystemExitやInterruptまで捕捉してしまう
begin
some_operation
rescue Exception => e
puts "エラー: #{e.message}"
end
# GOOD: StandardErrorを捕捉
begin
some_operation
rescue StandardError => e
puts "エラー: #{e.message}"
end
# または具体的な例外を指定
begin
some_operation
rescue ArgumentError, TypeError => e
puts "エラー: #{e.message}"
end
- 例外を制御フローに使わない
# BAD: 例外を制御フローに使用
def find_user(id)
user = users[id]
raise NotFoundError unless user
user
end
# GOOD: nilを返す
def find_user(id)
users[id]
end
# または
def find_user!(id)
users[id] || raise(NotFoundError, "User not found: #{id}")
end
ベストプラクティス
- 適切な例外クラスを使う
# GOOD: 明確な例外クラス
class User
def update_email(email)
raise ArgumentError, "メールアドレスが不正です" unless valid_email?(email)
@email = email
end
end
- 例外メッセージは具体的に
# BAD: 情報不足
raise "エラー"
# GOOD: 具体的なメッセージ
raise ArgumentError, "年齢は0〜150の範囲で指定してください(指定値: #{age})"
- ensure節でリソースを解放
# GOOD: リソースの確実な解放
def process_file(filename)
file = File.open(filename)
begin
# 処理
ensure
file.close
end
end
# さらに良い: ブロック付きopenを使う
def process_file(filename)
File.open(filename) do |file|
# 処理(自動的にcloseされる)
end
end
Ruby 3.4での改善点
- エラーメッセージの改善: より詳細でわかりやすいエラーメッセージ
- スタックトレースの最適化: デバッグがしやすくなった
- パフォーマンス向上: 例外処理のオーバーヘッドが削減
デバッグ情報の取得
begin
raise "エラー発生"
rescue => e
puts "クラス: #{e.class}"
puts "メッセージ: #{e.message}"
puts "バックトレース:"
puts e.backtrace.first(5) # 最初の5行
# 詳細情報
puts "\n詳細:"
puts e.full_message
end
まとめ
この記事では、例外処理について以下の内容を学びました:
- begin-rescue-ensure の基本構文
- 特定の例外の捕捉と複数例外の処理
- カスタム例外クラスの作成
- 実践的なエラーハンドリングパターン
- 注意点とベストプラクティス
例外処理を適切に使用することで、堅牢で保守性の高いRubyコードを書けるようになります。
Discussion