Rubyで堅牢なプログラムを作成する上で、例外処理の理解は避けて通れません。
「0での割り算」や「存在しないファイルを開こうとすること」など、プログラムの実行中に予期せぬエラーが発生すると、プログラムは即座に停止してしまいます。
「例外処理」とは、そのようなエラーが発生した場合でも、プログラムを強制終了させずに、開発者が意図した通りの回復処理やエラー通知を行えるようにする仕組みです。
この記事では、Rubyの例外処理の基本である begin-rescue-end 構文から、意図的に例外を発生させる raise、独自の例外クラス作成まで、具体的なサンプルコードを交えて初心者にもわかりやすく徹底的に解説します。
受講生から評判の良いプログラミングスクール
スクール |
特徴 |
受講料金 |
| 大手比較サイトで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で割ることはできません。
プログラムを終了します。
ソースコードの解説
beginブロック内で10 / 0という0除算を実行しようとします。ZeroDivisionErrorという例外が発生したため、beginブロック内のそれ以降の処理(puts "計算結果: #{result}")は実行されません。- プログラムは即座に
rescue節にジャンプします。 rescueブロック内のputs "エラーが発生しました:..."が実行されます。endの後、プログラムは停止せず、次の処理puts "プログラムを終了します。"へと進みます。- もし
denominatorが0ではなく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
実行結果
エラー:ファイルが見つかりません。
ファイルハンドルがありません(クローズ処理不要)。
ソースコードの解説
File.open("non_existent_file.txt")で、存在しないファイルを開こうとしたため、例外(Errno::ENOENT)が発生しました。rescue節が実行され、「エラー:ファイルが見つかりません。」と表示されます。else節はスキップされます。rescue節の実行が終わった後、最後にensure節が実行されます。file変数はnilのまま(ファイルを開けなかった)ため、if文のelse側が実行されます。- もしファイルが正常に開けていたとしても、
ensure節は実行され、ファイルのクローズ処理が行われます。
例外オブジェクトの補足と詳細の取得
rescue 節は、発生した例外に関する詳細な情報(エラーメッセージや発生箇所など)を受け取ることができます。
rescue => e で例外オブジェクトを取得する
rescue の後ろに => 変数名(慣例的に e や err が使われる)を記述することで、発生した例外オブジェクトを取得できます。
例外オブジェクトからは、主に以下の情報を取得可能です。
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.classでZeroDivisionErrorというクラス名が取得できます。e.messageでdivided 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
ソースコードの解説
check_age(20)は正常に実行されます。check_age(-5)が呼び出されると、if age < 0の条件が真となり、raise "..."が実行されます。raiseに文字列だけを渡した場合、デフォルトでRuntimeErrorという種類の例外が発生します。- 例外が発生したため、
check_ageメソッドは即座に中断され、呼び出し元のbeginブロックからrescue節へと制御が移ります。 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
アプリケーションを安全に終了します。
ソースコードの解説
complex_processメソッド内でZeroDivisionErrorが発生し、rescue節で補足されます。- ログ出力(
[LOG] ...)が実行されます。 raiseが実行され、補足したZeroDivisionErrorがcomplex_processメソッドの呼び出し元(メイン処理のbeginブロック)へと投げられます。- メイン処理側の
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 してはいけません。
(注:SyntaxError は Exception の子孫ですが、通常はプログラムの実行(パース)時点で発生するため、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 の省略
メソッド(def … end)定義の中では、begin と end を省略して rescue や ensure を直接書くことができます。
メソッド全体が暗黙的に 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_countがmax_retriesに達した場合はelse側に分岐し、リトライを中断します。retryは無限ループに陥る危険性があるため、必ずリトライ回数の上限を設けるようにしてください。
まとめ
この記事では、Rubyの例外処理について、その基本から応用までを解説しました。
なお、Rubyを体系的に学んだり、Rubyのスキルを高めたりするためには、プログラミングスクールを利用するのも有効です。
細かな疑問がすぐに解決するだけでなく、現役エンジニアが「質の高いポートフォリオ」を作成するための手助けをしてくれたり、エンジニア就職・転職のコツを教えてくれたりするなど、様々なメリットがありますので、独学に疲れた方は検討してみてはいかがでしょうか。
特にRuby+Railsを学ぶ場合は、群を抜いて評判の良い「RUNTEQ」を強くおすすめします。



