基礎

Ruby例外処理入門|begin-rescue-endの使い方

Ruby 例外処理 エラーハンドリング

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構文に相当します。

Ruby
begin
  # 例外が発生する可能性のある処理
  result = 10 / 0
rescue => e
  # 例外が発生した場合の処理
  puts "エラーが発生しました: \#{e.message}"
end
# 出力: エラーが発生しました: divided by 0

rescue => ee には例外オブジェクトが代入されます。e.message でエラーメッセージ、e.class で例外クラス名を取得できます。

Ruby
begin
  result = 10 / 0
rescue => e
  puts e.class    # ZeroDivisionError
  puts e.message  # divided by 0
  puts e.backtrace.first  # エラー発生箇所
end

特定の例外クラスを指定してrescueする

rescue に例外クラスを指定すると、そのクラス(およびサブクラス)の例外だけを捕捉できます。

Ruby
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 を書くことができます。

Ruby
def divide(a, b)
  a / b
rescue ZeroDivisionError
  puts "ゼロでは割れません"
  nil
end

divide(10, 0)  # ゼロでは割れません

注意rescue を引数なしで書くと StandardError とそのサブクラスのみ捕捉します。NoMemoryErrorSystemExit などの致命的なエラーは捕捉されません。これは意図的な設計です。

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のデフォルト対象
RuntimeErrorraiseのデフォルト例外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)を使いましょう。

Ruby(悪い例)
# 悪い例: Ctrl+C でも止まらなくなる
begin
  loop do
    puts "動作中..."
    sleep 1
  end
rescue Exception => e
  puts "捕捉: \#{e.class}"
  retry  # 永遠に止まらない!
end
Ruby(良い例)
# 良い例: StandardError だけを捕捉
begin
  # 処理
rescue StandardError => e
  puts "エラー: \#{e.message}"
end

Step 3ensure(後処理の保証)

ensure は例外の発生有無にかかわらず必ず実行されるブロックです。他の言語の finally に相当します。ファイルのクローズやDB接続の解放など、後始末が必要な場面で使います。

Ruby
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 の間に記述します。

Ruby
begin
  result = 10 / 2
rescue ZeroDivisionError => e
  puts "エラー: \#{e.message}"
else
  # 例外が起きなかった場合のみ実行
  puts "計算結果: \#{result}"
ensure
  puts "処理完了"
end
# 出力:
# 計算結果: 5
# 処理完了

ぼっち演算子 &.file&.close はぼっち演算子(safe navigation operator)を使っています。filenil の場合に NoMethodError を防ぎます。ensure内ではリソースが確保されていない可能性があるため、このパターンは頻出です。

注意:ensure ブロック内で return を使うと、rescue で捕捉した例外が握りつぶされます。ensure 内での return は避けましょう。

Step 4retry(リトライ処理)

retry を rescue ブロック内で使うと、begin ブロックの先頭から処理を再実行します。ネットワーク接続やAPI呼び出しなど、一時的な障害でリトライしたい場合に便利です。

Ruby
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 リクエストのリトライ

Ruby
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)を使うと、意図的に例外を発生させることができます。入力値の検証やビジネスロジックでのエラー通知に使います。

Ruby
# 文字列だけを渡す(RuntimeError が発生)
raise "何かがおかしい"

# 例外クラスとメッセージを指定
raise ArgumentError, "引数は正の整数で指定してください"

# 例外オブジェクトを直接渡す
raise TypeError.new("文字列が必要です")
raise の書き方発生する例外
raise直前の例外を再送出(rescue内)/ RuntimeError(それ以外)
raise "メッセージ"RuntimeError + メッセージ
raise ExceptionClass, "メッセージ"指定した例外クラス + メッセージ
raise ExceptionClass.new("メッセージ")例外オブジェクトを直接指定

実践的な例:入力値の検証

Ruby
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 を呼ぶと、捕捉した例外をそのまま再送出できます。ログを記録してから呼び出し元にエラーを伝えたい場合に使います。

Ruby
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 と failraisefail は完全に同じ動作をします。慣習として、通常のエラー通知には raise、回復不能なエラーには fail を使うこともありますが、多くのRubyプロジェクトでは raise に統一されています。

Step 6カスタム例外クラスの作成

独自の例外クラスを作成すると、アプリケーション固有のエラーを明確に分類・捕捉できます。カスタム例外は StandardError を継承して作成します。

Ruby
# 基本的なカスタム例外
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

階層的なカスタム例外

アプリケーションの規模が大きくなると、例外クラスも階層的に設計すると管理しやすくなります。

Ruby
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
Ruby(使用例)
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 で捕捉可能になり、InterruptSystemExit を意図せず捕捉するリスクを避けられます。

例外処理のベストプラクティス

  • rescue Exception は使わず、rescue StandardError またはクラス指定なしの rescue を使う
  • カスタム例外は StandardError を継承する
  • rescue は広い範囲ではなく、必要最小限のコードを囲む
  • retry には必ず上限回数を設定する
  • ensure でリソースの解放を確実に行う
  • rescue 内で例外を握りつぶさず、ログ記録や再送出を検討する
キーワード役割対応する他言語
begin例外処理ブロックの開始try
rescue例外の捕捉catch / except
else例外が発生しなかった場合else(Python)
ensure必ず実行される後処理finally
retrybegin からやり直し(Ruby固有)
raise / fail例外を発生させるthrow / raise