記事内にはプロモーションが含まれています

Rubyの例外処理を徹底解説!begin-rescue-endからraiseまで

Rubyの例外処理を徹底解説!begin-rescue-endからraiseまで プログラミングの疑問解決

Rubyで堅牢なプログラムを作成する上で、例外処理の理解は避けて通れません。

「0での割り算」や「存在しないファイルを開こうとすること」など、プログラムの実行中に予期せぬエラーが発生すると、プログラムは即座に停止してしまいます。

「例外処理」とは、そのようなエラーが発生した場合でも、プログラムを強制終了させずに、開発者が意図した通りの回復処理やエラー通知を行えるようにする仕組みです。

この記事では、Rubyの例外処理の基本である begin-rescue-end 構文から、意図的に例外を発生させる raise、独自の例外クラス作成まで、具体的なサンプルコードを交えて初心者にもわかりやすく徹底的に解説します。

【著者プロフィール&本記事の信頼性】
プロフィール
  • 著者は元エンジニア
  • 大手プログラミングスクールのWebディレクター兼 ライターを経験
  • 自らも地元密着型のプログラミングスクールを運営
プロフィール詳細はコチラ
忖度一切なし!
受講生から評判の良いプログラミングスクール
スクール
特徴
受講料金
大手比較サイトで4年連続人気NO.1!受講生からの評判も非常に高く、Web系のエンジニアを目指すならRUNTEQ一択。
657,000円
(最大約53万円の給付金が適用される)
月単価80万円以上の現役エンジニア講師による指導!一度入会すればサポートは半永久的。
498,000円
格安で質の高いWeb制作スキルを習得したい人におすすめ!業界最安級の料金でありながら、コミュニティやサポートが充実。
129,800円~
完全無料でプログラミングが学べる貴重なスクール!最短1ヶ月で卒業可能。ゼロスク運営会社への就職もできる。
完全無料
長期間に渡って学習し、希少人材を目指す人に最適なスクール!受講料は高いものの、高収入を得られる人材を目指せる。
96~132万円

Ruby例外処理の基本構文 begin-rescue-end

Rubyにおける例外処理の最も基本的な構文が begin-rescue-end ブロックです。

正常な処理、エラー発生時の処理、そしてエラーの有無にかかわらず実行する処理を、明確に分けて記述できます。

begin rescue end の基本的な書き方

基本の形は、エラーが発生する可能性のある処理を begin 節に、エラーが発生した場合の処理を rescue 節に記述します。

有名なエラーとして「0除算エラー(ZeroDivisionError)」があります。
数値は0で割ることができないため、実行すると例外が発生します。

begin
  # 例外が発生する可能性のある処理
  numerator = 10
  denominator = 0
  
  result = numerator / denominator # ここで ZeroDivisionError が発生
  
  puts "計算結果: #{result}"
  
rescue
  # 例外が発生した時に実行される処理
  puts "エラーが発生しました:0で割ることはできません。"
end

puts "プログラムを終了します。"

実行結果

エラーが発生しました:0で割ることはできません。
プログラムを終了します。

ソースコードの解説

  1. begin ブロック内で 10 / 0 という0除算を実行しようとします。
  2. ZeroDivisionError という例外が発生したため、begin ブロック内のそれ以降の処理(puts "計算結果: #{result}")は実行されません。
  3. プログラムは即座に rescue 節にジャンプします。
  4. rescue ブロック内の puts "エラーが発生しました:..." が実行されます。
  5. end の後、プログラムは停止せず、次の処理 puts "プログラムを終了します。" へと進みます。
  6. もし denominator0 ではなく 2 であった場合、begin 節は正常に完了し、rescue 節は実行されません。

else 節:例外が発生しなかった場合の処理

rescue 節の後に else 節を追加できます。

else 節は、begin 節の処理が例外を発生させることなく、最後まで正常に完了した場合にのみ実行されるブロックです。

begin
  numerator = 10
  denominator = 2 # 正常な値に変更
  
  result = numerator / denominator
  
rescue
  puts "エラーが発生しました。"
  
else
  # 例外が発生しなかった場合のみ実行
  puts "計算は正常に完了しました。"
  puts "計算結果: #{result}"
end

実行結果

計算は正常に完了しました。
計算結果: 5

ソースコードの解説

  • 10 / 2 は正常に実行されるため、例外は発生しません。
  • rescue 節はスキップされます。
  • begin 節が正常に完了したため、else 節が実行されました。
  • else 節は「成功時の処理」を begin 節の主要なロジックから分離できるため、コードが読みやすくなります。

ensure 節:必ず実行したい後処理

ensure 節は、例外が発生しようがしまいが、begin ブロックを抜ける前に「必ず最後に実行される」処理を記述するためのブロックです。

ファイルのクローズ処理や、データベース接続の切断など、プログラムの状態を元に戻すための「後片付け」によく利用されます。

file = nil
begin
  file = File.open("non_existent_file.txt") # 存在しないファイル (Errno::ENOENT)
  puts "ファイルを開きました。"
  
rescue
  puts "エラー:ファイルが見つかりません。"
  
else
  puts "ファイル処理が正常に完了しました。"
  
ensure
  # 例外が発生しても、しなくても、必ず実行
  if file
    file.close
    puts "ファイルをクローズしました。"
  else
    puts "ファイルハンドルがありません(クローズ処理不要)。"
  end
end

実行結果

エラー:ファイルが見つかりません。
ファイルハンドルがありません(クローズ処理不要)。

ソースコードの解説

  1. File.open("non_existent_file.txt") で、存在しないファイルを開こうとしたため、例外(Errno::ENOENT)が発生しました。
  2. rescue 節が実行され、「エラー:ファイルが見つかりません。」と表示されます。
  3. else 節はスキップされます。
  4. rescue 節の実行が終わった後、最後に ensure 節が実行されます。
  5. file 変数は nil のまま(ファイルを開けなかった)ため、if文の else 側が実行されます。
  6. もしファイルが正常に開けていたとしても、ensure 節は実行され、ファイルのクローズ処理が行われます。

例外オブジェクトの補足と詳細の取得

rescue 節は、発生した例外に関する詳細な情報(エラーメッセージや発生箇所など)を受け取ることができます。

rescue => e で例外オブジェクトを取得する

rescue の後ろに => 変数名(慣例的に eerr が使われる)を記述することで、発生した例外オブジェクトを取得できます。

例外オブジェクトからは、主に以下の情報を取得可能です。

  • e.message : エラーメッセージ(「なぜ」エラーになったか)
  • e.class : 例外クラス名(「どの種類」のエラーか)
  • e.backtrace : エラーが発生するまでのメソッド呼び出し履歴(「どこで」エラーになったか)
begin
  10 / 0
rescue => e
  puts "--- 例外オブジェクトの情報 ---"
  puts "クラス: #{e.class}"
  puts "メッセージ: #{e.message}"
  puts "--- バックトレース(抜粋) ---"
  puts e.backtrace[0] # 通常、バックトレースの先頭が発生箇所
end

実行結果

--- 例外オブジェクトの情報 ---
クラス: ZeroDivisionError
メッセージ: divided by 0
--- バックトレース(抜粋) ---
(実行したファイル名):2:in `/'

ソースコードの解説

  • rescue => e と記述したことで、発生した ZeroDivisionError のインスタンスが変数 e に代入されました。
  • e.classZeroDivisionError というクラス名が取得できます。
  • e.messagedivided by 0 という具体的なエラー理由が取得できました。
  • e.backtrace は配列になっており、[0]でエラーの発生箇所(ファイル名と行番号、メソッド名)を確認できます。

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

rescue は、デフォルトでは StandardError とそのサブクラス(ほとんどの一般的なエラー)を補足します。

特定の例外クラスのみを補足対象としたい場合は、rescue の直後にクラス名を指定します。

begin
  # どちらかの行をコメントアウトして試してください
  # 10 / 0       # ZeroDivisionError
  "hello".world # NoMethodError
  
rescue ZeroDivisionError => e
  puts "0除算エラーを補足しました: #{e.message}"
  
rescue NameError => e
  puts "名前エラーを補足しました: #{e.message}"
  
rescue NoMethodError => e
  puts "メソッド呼び出しエラーを補足しました: #{e.message}"
end

実行結果("hello".world の場合)

メソッド呼び出しエラーを補足しました: undefined method `world' for "hello":String

ソースコードの解説

  • rescue 節を複数記述することで、例外の種類に応じた処理の分岐が可能です。
  • "hello".world を実行すると NoMethodError が発生します。
  • プログラムは、上から順に rescue 節を見ていき、rescue NoMethodError に該当したため、そのブロックが実行されました。
  • もし 10 / 0 を実行した場合、rescue ZeroDivisionError のブロックが実行されます。
  • もし rescue で指定していない種類のエラー(例: TypeError)が発生した場合、その例外は補足されず、プログラムは停止します。

複数の例外クラスを一度に補足する方法

複数の異なる例外クラスに対して、同じ処理を行いたい場合は、rescue 節にカンマ(,)区切りでまとめて指定できます。

begin
  # 10 / 0
  "hello".world
  
rescue ZeroDivisionError, NoMethodError => e
  puts "計算エラー、または メソッド呼び出しエラーが発生しました。"
  puts "エラークラス: #{e.class}"
  
rescue => e
  # 上記以外、全ての StandardError
  puts "その他のエラーが発生しました: #{e.class}"
end

実行結果

計算エラー、または メソッド呼び出しエラーが発生しました。
エラークラス: NoMethodError

ソースコードの解説

  • ZeroDivisionError または NoMethodError が発生した場合、1つ目の rescue ブロックが実行されます。
  • 複数の rescue を書く場合、最後に rescue => e(クラス指定なし)を配置しておくと、それまでに指定されなかった全ての StandardError をまとめて補足する「キャッチオール」として機能します。

意図的に例外を発生させる raise の使い方

プログラムが予期しないエラーを補足するだけでなく、開発者が「これは異常な状態だ」と判断したときに、意図的に例外を発生させることも重要です。

raise で例外を発生させる方法

raise メソッドを使うと、例外を強制的に発生させることができます。

def check_age(age)
  if age < 0
    # RuntimeError(実行時エラー)を発生させる
    raise "年齢にマイナスの値が指定されました: #{age}"
  end
  puts "#{age}歳です。正常な値です。"
end

begin
  check_age(20)
  check_age(-5) # ここで例外が発生
  check_age(30) # ここは実行されない
  
rescue => e
  puts "処理を中断しました: #{e.message}"
end

実行結果

20歳です。正常な値です。
処理を中断しました: 年齢にマイナスの値が指定されました: -5

ソースコードの解説

  1. check_age(20) は正常に実行されます。
  2. check_age(-5) が呼び出されると、if age < 0 の条件が真となり、raise "..." が実行されます。
  3. raise に文字列だけを渡した場合、デフォルトで RuntimeError という種類の例外が発生します。
  4. 例外が発生したため、check_age メソッドは即座に中断され、呼び出し元の begin ブロックから rescue 節へと制御が移ります。
  5. e.message には raise で指定した文字列が格納されます。

特定の例外クラスを指定して raise することも可能です。

# 組み込みの例外クラス ArgumentError(引数エラー)を指定
raise ArgumentError, "年齢にマイナスの値が指定されました: #{age}"

rescue 節で例外を再スロー(raise)する

rescue 節で例外を補足した後、その処理を上位の呼び出し元にも伝えたい場合があります。

rescue 節の中で引数なしの raise を実行すると、補足した例外をそのまま再スローできます。

def complex_process
  begin
    10 / 0
  rescue ZeroDivisionError => e
    # 1. ここでエラーログを記録する
    puts "[LOG] 致命的な計算エラーが発生しました: #{e.message}"
    
    # 2. 処理を続行せず、上位に例外をそのまま伝える
    raise
  end
end

# メインの処理
begin
  complex_process
rescue => e
  # complex_process から再スローされた例外をここでキャッチ
  puts "メイン処理側でエラーを補足しました。クラス: #{e.class}"
  puts "アプリケーションを安全に終了します。"
end

実行結果

[LOG] 致命的な計算エラーが発生しました: divided by 0
メイン処理側でエラーを補足しました。クラス: ZeroDivisionError
アプリケーションを安全に終了します。

ソースコードの解説

  1. complex_process メソッド内で ZeroDivisionError が発生し、rescue 節で補足されます。
  2. ログ出力([LOG] ...)が実行されます。
  3. raise が実行され、補足した ZeroDivisionErrorcomplex_process メソッドの呼び出し元(メイン処理の begin ブロック)へと投げられます。
  4. メイン処理側の rescue => e が、再スローされた例外をキャッチし、最終的な処理を行います。

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

ここでは、Rubyの例外処理をより深く理解し、適切に扱うための重要なルールを解説します。

StandardError と Exception の違い

Rubyの例外クラスは階層構造になっています。

その頂点付近に Exception クラスがあり、その子クラスとして StandardError があります。

  • Exception: Interrupt(Ctrl+Cでの中断)や NoMemoryError(メモリ不足)、SyntaxError(文法エラー)など、プログラムの実行継続がほぼ不可能な、システムレベルの深刻なエラーを含みます。
  • StandardError: RuntimeError, ZeroDivisionError, NoMethodError, ArgumentError など、通常のアプリケーションで発生しうる、回復可能なエラーの基底クラスです。

rescue(クラス指定なし)が捕捉するのは、この StandardError とその子孫だけです。

begin
  # ...
rescue Exception => e
  # これは「絶対に」やってはいけない書き方です!
  puts "システムエラー: #{e.class}"
end

上記のように rescue Exception と記述すると、Ctrl+Cによるプログラムの中断 (Interrupt) までも捕捉してしまい、プログラムを正常に終了させることすら困難になります。

例外を捕捉する際は、原則として StandardError(またはその子クラス)のみを対象とし、Exception を直接 rescue してはいけません。

(注:SyntaxErrorException の子孫ですが、通常はプログラムの実行(パース)時点で発生するため、begin-rescue ブロックで捕捉することはできません。eval 内などで動的にコードを実行した場合は捕捉可能です。)

独自の例外クラスを作成する(カスタム例外)

アプリケーション固有のエラー状態を表したい場合、独自の例外クラスを定義することが推奨されます。

例外クラスは、必ず StandardError(またはその子孫)を継承して作成してください。

# アプリケーション固有の(バリデーション)エラー
class InvalidUserInputError < StandardError
end

def register_user(username)
  if username.nil? || username.length < 4
    # 独自例外を発生させる
    raise InvalidUserInputError, "ユーザー名は4文字以上必要です。"
  end
  puts "ユーザー「#{username}」を登録しました。"
end

begin
  register_user("user123")
  register_user("abc") # 例外発生
  
rescue InvalidUserInputError => e
  # 独自例外を捕捉
  puts "入力エラー: #{e.message}"
rescue => e
  # その他のエラー
  puts "予期せぬエラー: #{e.message}"
end

実行結果

ユーザー「user123」を登録しました。
入力エラー: ユーザー名は4文字以上必要です。

ソースコードの解説

  • class InvalidUserInputError < StandardError で、StandardError を継承した独自の例外クラスを定義しました(中身は空で構いません)。
  • register_user メソッド内で、バリデーション違反時に raise InvalidUserInputError を呼び出しています。
  • 呼び出し元で rescue InvalidUserInputError と指定できるため、RuntimeError などを使うよりも、エラーの原因が明確になります。

メソッド内での begin の省略

メソッド(defend)定義の中では、beginend を省略して rescueensure を直接書くことができます。

メソッド全体が暗黙的に begin ブロックとして扱われます。

def divide(a, b)
  result = a / b
  puts "結果: #{result}"
  
rescue ZeroDivisionError => e
  puts "メソッド内でエラーを捕捉: #{e.message}"
  
ensure
  puts "メソッドの処理を終了します。"
end

divide(10, 2)
divide(10, 0)

実行結果

結果: 5
メソッドの処理を終了します。
メソッド内でエラーを捕捉: divided by 0
メソッドの処理を終了します。

ソースコードの解説

  • def ... end ブロック全体が begin ... end のように振る舞います。
  • divide(10, 0) が実行されると、メソッド内の rescue 節が直接呼び出され、その後に ensure 節が実行されました。
  • この記法は、メソッド全体のエラーを処理する場合にコードを簡潔にできます。

retry による処理の再試行

rescue 節の中で retry キーワードを使用すると、begin ブロックの処理を最初からやり直すことができます。

max_retries = 3
retry_count = 0

begin
  retry_count += 1
  puts "処理を実行します... (試行回数: #{retry_count})"
  
  # ネットワーク接続のシミュレーション(ランダムで失敗)
  if rand(3) == 0 # 1/3の確率で失敗
    raise "ネットワークエラー"
  end

  puts "処理に成功しました。"
  
rescue => e
  if retry_count < max_retries
    puts "エラー発生。リトライします。(#{e.message})"
    sleep(1) # 1秒待機
    retry # begin の先頭に戻る
  else
    puts "リトライ上限(#{max_retries}回)に達しました。処理を中断します。"
  end
end

実行結果(例)

処理を実行します... (試行回数: 1)
エラー発生。リトライします。(ネットワークエラー)
処理を実行します... (試行回数: 2)
処理を実行します... (試行回数: 3)
エラー発生。リトライします。(ネットワークエラー)
リトライ上限(3回)に達しました。処理を中断します。

ソースコードの解説

  • max_retries でリトライの上限回数を設定し、retry_count で現在の試行回数を管理します。
  • begin ブロックの先頭で retry_count をインクリメント(加算)することで、puts で表示される試行回数と実際の回数が一致します。
  • rescue 節でエラーを捕捉した際、試行回数が上限未満であれば retry を実行し、begin の先頭に戻ります。
  • retry_countmax_retries に達した場合は else 側に分岐し、リトライを中断します。
  • retry は無限ループに陥る危険性があるため、必ずリトライ回数の上限を設けるようにしてください。

まとめ

この記事では、Rubyの例外処理について、その基本から応用までを解説しました。

なお、Rubyを体系的に学んだり、Rubyのスキルを高めたりするためには、プログラミングスクールを利用するのも有効です。

細かな疑問がすぐに解決するだけでなく、現役エンジニアが「質の高いポートフォリオ」を作成するための手助けをしてくれたり、エンジニア就職・転職のコツを教えてくれたりするなど、様々なメリットがありますので、独学に疲れた方は検討してみてはいかがでしょうか。

特にRuby+Railsを学ぶ場合は、群を抜いて評判の良い「RUNTEQ」を強くおすすめします。

Follow me!

PAGE TOP