Ruby例外処理入門
begin-rescue-endの使い方
Rubyの例外処理の仕組みを基礎から解説。begin-rescue-end、ensure、retry、raiseの使い方からカスタム例外クラスの作成まで学べます。
こんな人向けの記事です
- Rubyの例外処理を基礎から学びたい
- rescue/ensure/retryの使い分けを理解したい
- カスタム例外クラスを作成したい
Step 1例外処理の基本(begin-rescue-end)
Rubyではbegin-rescue-end構文を使って例外(エラー)を捕捉し、プログラムの異常終了を防ぐことができます。他の言語のtry-catch構文に相当します。
begin
# 例外が発生する可能性のある処理
result = 10 / 0
rescue => e
# 例外が発生した場合の処理
puts "エラーが発生しました: \#{e.message}"
end
# 出力: エラーが発生しました: divided by 0
rescue => e の e には例外オブジェクトが代入されます。e.message でエラーメッセージ、e.class で例外クラス名を取得できます。
begin
result = 10 / 0
rescue => e
puts e.class # ZeroDivisionError
puts e.message # divided by 0
puts e.backtrace.first # エラー発生箇所
end
特定の例外クラスを指定してrescueする
rescue に例外クラスを指定すると、そのクラス(およびサブクラス)の例外だけを捕捉できます。
begin
num = Integer("abc")
rescue ArgumentError => e
puts "引数が不正です: \#{e.message}"
rescue ZeroDivisionError => e
puts "ゼロ除算です: \#{e.message}"
end
# 出力: 引数が不正です: invalid value for Integer(): "abc"
rescue の省略形:メソッド全体を例外処理したい場合は、begin/end を省略してメソッド定義の中に直接 rescue を書くことができます。
def divide(a, b)
a / b
rescue ZeroDivisionError
puts "ゼロでは割れません"
nil
end
divide(10, 0) # ゼロでは割れません
注意:rescue を引数なしで書くと StandardError とそのサブクラスのみ捕捉します。NoMemoryError や SystemExit などの致命的なエラーは捕捉されません。これは意図的な設計です。
Step 2例外クラスの種類
Rubyの例外クラスは階層構造になっています。全ての例外は Exception クラスを頂点とする継承ツリーに属しています。
Exception
+-- NoMemoryError
+-- ScriptError
| +-- SyntaxError
| +-- LoadError
+-- SignalException
| +-- Interrupt(Ctrl+C)
+-- SystemExit
+-- StandardError(rescue のデフォルト対象)
+-- ArgumentError
+-- IOError
| +-- EOFError
+-- IndexError
| +-- KeyError
| +-- StopIteration
+-- NameError
| +-- NoMethodError
+-- RangeError
| +-- FloatDomainError
+-- RuntimeError(raise のデフォルト)
+-- TypeError
+-- ZeroDivisionError
| 例外クラス | 発生する場面 | 例 |
|---|---|---|
StandardError | 一般的なエラーの基底クラス | rescueのデフォルト対象 |
RuntimeError | raiseのデフォルト例外 | raise "エラー" |
ArgumentError | 引数の数や値が不正 | Integer("abc") |
TypeError | 型が不正 | "abc" + 123 |
NameError | 未定義の変数/定数を参照 | puts undefined_var |
NoMethodError | 未定義のメソッドを呼び出し | nil.foo |
ZeroDivisionError | ゼロによる除算 | 1 / 0 |
IOError | 入出力エラー | 閉じたファイルへの書き込み |
Errno::ENOENT | ファイルが存在しない | File.open("not_exist") |
注意:rescue Exception => e と書くと Interrupt(Ctrl+C)や SystemExit(exit呼び出し)まで捕捉してしまい、プログラムを正常に終了できなくなります。通常は rescue StandardError(またはクラス指定なしの rescue)を使いましょう。
# 悪い例: Ctrl+C でも止まらなくなる
begin
loop do
puts "動作中..."
sleep 1
end
rescue Exception => e
puts "捕捉: \#{e.class}"
retry # 永遠に止まらない!
end
# 良い例: StandardError だけを捕捉
begin
# 処理
rescue StandardError => e
puts "エラー: \#{e.message}"
end
Step 3ensure(後処理の保証)
ensure は例外の発生有無にかかわらず必ず実行されるブロックです。他の言語の finally に相当します。ファイルのクローズやDB接続の解放など、後始末が必要な場面で使います。
begin
file = File.open("data.txt", "r")
data = file.read
puts data
rescue Errno::ENOENT => e
puts "ファイルが見つかりません: \#{e.message}"
rescue IOError => e
puts "読み込みエラー: \#{e.message}"
ensure
# 例外が起きてもここは必ず実行される
file&.close
puts "ファイル処理を終了しました"
end
| ブロック | 実行タイミング | 用途 |
|---|---|---|
begin | 最初に実行 | メインの処理 |
rescue | 例外発生時のみ | エラーハンドリング |
else | 例外が発生しなかった場合 | 正常時の追加処理 |
ensure | 常に実行 | 後始末(リソース解放) |
else ブロック
else は例外が発生しなかった場合にのみ実行されるブロックです。rescue と ensure の間に記述します。
begin
result = 10 / 2
rescue ZeroDivisionError => e
puts "エラー: \#{e.message}"
else
# 例外が起きなかった場合のみ実行
puts "計算結果: \#{result}"
ensure
puts "処理完了"
end
# 出力:
# 計算結果: 5
# 処理完了
ぼっち演算子 &.:file&.close はぼっち演算子(safe navigation operator)を使っています。file が nil の場合に NoMethodError を防ぎます。ensure内ではリソースが確保されていない可能性があるため、このパターンは頻出です。
注意:ensure ブロック内で return を使うと、rescue で捕捉した例外が握りつぶされます。ensure 内での return は避けましょう。
Step 4retry(リトライ処理)
retry を rescue ブロック内で使うと、begin ブロックの先頭から処理を再実行します。ネットワーク接続やAPI呼び出しなど、一時的な障害でリトライしたい場合に便利です。
retries = 0
begin
puts "試行 \#{retries + 1} 回目..."
# 一時的に失敗する可能性のある処理
raise "接続エラー" if retries < 3
puts "成功しました!"
rescue => e
retries += 1
if retries <= 3
puts "リトライします: \#{e.message}"
sleep 1 # 少し待ってからリトライ
retry
else
puts "リトライ上限に達しました"
raise # 例外を再送出
end
end
注意:retry にはリトライ回数の上限を必ず設けてください。上限なしで retry すると無限ループに陥ります。
実践的な例:HTTP リクエストのリトライ
require "net/http"
require "uri"
MAX_RETRIES = 3
def fetch_with_retry(url)
retries = 0
begin
uri = URI.parse(url)
response = Net::HTTP.get_response(uri)
case response
when Net::HTTPSuccess
response.body
when Net::HTTPServerError
raise "サーバーエラー: \#{response.code}"
else
raise "予期しないレスポンス: \#{response.code}"
end
rescue StandardError => e
retries += 1
if retries <= MAX_RETRIES
wait_time = 2 ** retries # 指数バックオフ: 2, 4, 8秒
puts "リトライ \#{retries}/\#{MAX_RETRIES}(\#{wait_time}秒後): \#{e.message}"
sleep wait_time
retry
else
raise
end
end
end
指数バックオフ:リトライの待機時間を 2 ** retries(2の累乗)で増やすパターンを指数バックオフといいます。サーバーへの負荷を軽減しつつ、一時的な障害からの回復を期待できます。
Step 5raise(例外を発生させる)
raise(または fail)を使うと、意図的に例外を発生させることができます。入力値の検証やビジネスロジックでのエラー通知に使います。
# 文字列だけを渡す(RuntimeError が発生)
raise "何かがおかしい"
# 例外クラスとメッセージを指定
raise ArgumentError, "引数は正の整数で指定してください"
# 例外オブジェクトを直接渡す
raise TypeError.new("文字列が必要です")
| raise の書き方 | 発生する例外 |
|---|---|
raise | 直前の例外を再送出(rescue内)/ RuntimeError(それ以外) |
raise "メッセージ" | RuntimeError + メッセージ |
raise ExceptionClass, "メッセージ" | 指定した例外クラス + メッセージ |
raise ExceptionClass.new("メッセージ") | 例外オブジェクトを直接指定 |
実践的な例:入力値の検証
def withdraw(balance, amount)
raise ArgumentError, "金額は正の数で指定してください" unless amount.positive?
raise "残高不足です(残高: \#{balance}円、出金: \#{amount}円)" if amount > balance
balance - amount
end
begin
new_balance = withdraw(1000, 1500)
rescue ArgumentError => e
puts "引数エラー: \#{e.message}"
rescue RuntimeError => e
puts "処理エラー: \#{e.message}"
end
# 出力: 処理エラー: 残高不足です(残高: 1000円、出金: 1500円)
rescue 内での再送出
rescue ブロック内で引数なしの raise を呼ぶと、捕捉した例外をそのまま再送出できます。ログを記録してから呼び出し元にエラーを伝えたい場合に使います。
def process_data(data)
begin
result = data.map { |item| item.upcase }
rescue NoMethodError => e
puts "[ERROR] データ処理に失敗: \#{e.message}"
raise # 同じ例外を再送出
end
end
begin
process_data(["hello", nil, "world"])
rescue NoMethodError => e
puts "呼び出し元で捕捉: \#{e.message}"
end
# 出力:
# [ERROR] データ処理に失敗: undefined method 'upcase' for nil
# 呼び出し元で捕捉: undefined method 'upcase' for nil
raise と fail:raise と fail は完全に同じ動作をします。慣習として、通常のエラー通知には raise、回復不能なエラーには fail を使うこともありますが、多くのRubyプロジェクトでは raise に統一されています。
Step 6カスタム例外クラスの作成
独自の例外クラスを作成すると、アプリケーション固有のエラーを明確に分類・捕捉できます。カスタム例外は StandardError を継承して作成します。
# 基本的なカスタム例外
class AppError < StandardError; end
# メッセージ付きのカスタム例外
class ValidationError < StandardError
def initialize(field, message = "が不正です")
@field = field
super("\#{field}\#{message}")
end
attr_reader :field
end
begin
raise ValidationError.new("メールアドレス", "の形式が正しくありません")
rescue ValidationError => e
puts e.message # メールアドレスの形式が正しくありません
puts e.field # メールアドレス
end
階層的なカスタム例外
アプリケーションの規模が大きくなると、例外クラスも階層的に設計すると管理しやすくなります。
module MyApp
# アプリ共通の基底例外
class Error < StandardError; end
# 認証関連
class AuthenticationError < Error; end
class InvalidTokenError < AuthenticationError; end
class ExpiredTokenError < AuthenticationError; end
# データ関連
class DataError < Error; end
class RecordNotFoundError < DataError
def initialize(model, id)
super("\#{model} (ID: \#{id}) が見つかりません")
end
end
class DuplicateRecordError < DataError; end
# 外部サービス関連
class ExternalServiceError < Error; end
class ApiTimeoutError < ExternalServiceError; end
class ApiRateLimitError < ExternalServiceError; end
end
def find_user(id)
user = users.find { |u| u[:id] == id }
raise MyApp::RecordNotFoundError.new("User", id) unless user
user
end
begin
user = find_user(999)
rescue MyApp::AuthenticationError => e
puts "認証エラー: \#{e.message}"
rescue MyApp::DataError => e
# RecordNotFoundError, DuplicateRecordError をまとめて捕捉
puts "データエラー: \#{e.message}"
rescue MyApp::Error => e
# 上記以外のアプリエラーを捕捉
puts "アプリエラー: \#{e.message}"
end
# 出力: データエラー: User (ID: 999) が見つかりません
StandardError を継承する理由:Exception ではなく StandardError を継承するのが重要です。これにより、引数なしの rescue で捕捉可能になり、Interrupt や SystemExit を意図せず捕捉するリスクを避けられます。
例外処理のベストプラクティス
rescue Exceptionは使わず、rescue StandardErrorまたはクラス指定なしのrescueを使う- カスタム例外は
StandardErrorを継承する - rescue は広い範囲ではなく、必要最小限のコードを囲む
- retry には必ず上限回数を設定する
- ensure でリソースの解放を確実に行う
- rescue 内で例外を握りつぶさず、ログ記録や再送出を検討する
| キーワード | 役割 | 対応する他言語 |
|---|---|---|
begin | 例外処理ブロックの開始 | try |
rescue | 例外の捕捉 | catch / except |
else | 例外が発生しなかった場合 | else(Python) |
ensure | 必ず実行される後処理 | finally |
retry | begin からやり直し | (Ruby固有) |
raise / fail | 例外を発生させる | throw / raise |