【Ruby】rescueとは?初心者向けにわかりやすく解説

はじめに

Rubyでプログラミングを始めると、必ずといっていいほど出合うのが例外処理です。
例外処理を行わないと、エラー発生時にプログラムが予期せず終了してしまいます。
日常的な開発環境でも、ファイル読み込みやAPI通信などさまざまな場面でエラーは起こりがちです。
そんなときに活躍するのがrescueという構文になります。

このrescueを適切に使うことで、プログラムの途中でエラーが起きても安全に処理を続行したり、ユーザーにエラーメッセージを見やすく表示したりといった工夫が可能になります。
本記事では、Ruby初学者の方でも分かりやすいように、rescueの基本構文と実務における活用シーンを丁寧に解説していきます。

この記事を読むとわかること

  • Rubyにおける例外処理の基礎
  • rescueを使ったエラーハンドリングの具体的な書き方
  • 実務で想定される例外の発生場面と対処方法
  • rescueの複数例外処理やエラー情報の取得方法
  • ensureやretryを組み合わせた拡張的な書き方

ここでは主にRubyのコード例を交えながら進めていきます。
なお、技術的な側面だけでなく、実務でも起きがちな場面にそって解説することで、より実践的にイメージできるはずです。

rescueとは何か?~Rubyの例外処理の基本

Rubyにおける例外処理は、エラーが起きる可能性がある箇所を明示し、起きた場合にどう対応するかを定義するための仕組みです。
その中心となるのがrescueで、以下のような基本構文で使われます。

begin
  # エラーが起こるかもしれない処理
rescue
  # エラーが起きたときの処理
end

この構文は初心者の方にも理解しやすいと思いますが、実務では少し複雑な形で活用されることがあります。
たとえば、どんな種類のエラーをキャッチするかを指定したり、複数の異なるエラーを別々に処理したりするケースです。

具体的な利用イメージ

よくある例としては、ファイル操作や外部APIとの通信が挙げられます。
たとえば、ファイルを読み込むコードを書いた時に、指定したファイルが見つからなければエラーが起きますよね。
そのままだとプログラムが停止するため、rescueで補足してユーザーに「ファイルが見つかりませんでした」と表示したり、ログを記録して処理を継続したりします。

begin
  data = File.read("data.txt")  # 存在しないファイルを読み込もうとするとエラーが発生
  puts data
rescue
  puts "ファイルが見つかりませんでした"
end

これがRubyにおけるrescueの最もシンプルな使用例と言えるでしょう。
もし実務で、ファイルが存在しなくても後続の処理を継続したい場合には、このように例外処理を使うことでプログラムの異常終了を防げます。

エラー情報を取得する方法

rescueには、エラーの詳細をオブジェクトとして受け取る方法があります。
以下のように書くと、StandardErrorから派生したエラーオブジェクトが変数eに格納され、そのメッセージを参照できます。

begin
  data = File.read("data.txt")
rescue => e
  puts "エラーが発生しました: #{e.message}"
end

実際の開発現場では、捕捉したエラーをログに書き込んだり、メール通知やSlack通知などを行ったりすることもよくあります。
単なるエラーメッセージの表示だけでなく、トラブルシューティングの手がかりとしてエラーオブジェクトを活用することが多いです。

rescueを使うメリットと実務での活用シーン

rescueの一番のメリットは、エラー発生時の動作をコントロールできることです。
ユーザー向けアプリケーションであれば、ユーザーに正しいメッセージを返して再入力を求めたり、サーバーサイドの処理であればログや監視システムに情報を送ったりすることができます。

Webアプリケーションでの一例

Ruby on RailsのようなWebフレームワークを利用する場合、コントローラの中で何らかの処理を行う際に、rescueを使って以下のような対策を行うケースがあります。

def create
  @item = Item.new(item_params)
  @item.save!
  # ここで例外が発生しなければ、保存は成功
  redirect_to items_path, notice: "保存に成功しました"
rescue => e
  # 保存に失敗した場合や他のエラーが発生した場合
  logger.error "アイテムの保存に失敗: #{e.message}"
  render :new, alert: "保存に失敗しました"
end

例えば、このようにデータベース保存失敗時にはエラーを補足し、画面に戻すように書くことがよくあります。
ただエラーを表示するだけでなく、ユーザーが入力を再度できるようフォームを表示し直すなど、 UX (ユーザー体験)を高めるためにもrescueは欠かせません。

外部システムへの通信エラー対策

もう一つの典型的なシーンは、外部のシステムとやりとりする際の通信エラーです。
ネットワークが不安定だったり、相手側のシステムがダウンしている可能性があります。
この場合、処理を続行するかリトライを行うか、あるいはユーザーにエラーメッセージを伝えるかなど、状況に応じた対応を行わないといけません。

require "net/http"

begin
  response = Net::HTTP.get(URI("https://example.com/api/data"))
  puts "取得したデータ: #{response}"
rescue => e
  puts "外部システムからのデータ取得に失敗しました: #{e.message}"
end

このように書いておけば、外部システムがダウンしている際にも、アプリケーション全体が停止するのを避けられます。
エラー内容を記録し、必要に応じて再試行するロジックを追加していくことも多いです。

rescueで複数のエラーをキャッチする

実際の開発では、一つのコードブロック内で複数の種類のエラーが起きることがあります。
そんなときは、rescue節を複数書いて異なるエラーごとに処理を変える方法があります。

begin
  # 何かしらの処理
rescue ArgumentError => e
  puts "引数のエラーが発生しました: #{e.message}"
rescue IOError => e
  puts "入出力関連のエラーが発生しました: #{e.message}"
rescue => e
  puts "その他のエラーが発生しました: #{e.message}"
end

上記の例だと、ArgumentErrorが起きた場合はそのエラーだけを捕捉して特別な処理を行い、それ以外は最後のrescueに処理を委ねる形になっています。
Webアプリケーションでも「ユーザー入力に問題がある場合」と「サーバー内部の問題がある場合」で分けてハンドリングすることがあるため、このような複数のrescueを使う場面は珍しくありません。

実務への適用例

例えば、ユーザーの入力チェックをするメソッドがあり、それが不正な文字列を受け取るとArgumentErrorを発生させるとします。
同時に、ネットワーク経由でファイルを読み込んでいて、そちらがIOErrorを引き起こすかもしれない、というコードを書くこともあるでしょう。
その場合、どちらがエラーを起こしたかによって対応方法が全く異なるため、分けてキャッチすることが合理的です。

begin
  check_user_input(params[:input])  # 不正な入力だとArgumentErrorを発生
  data = File.read("/path/to/resource") # ファイル読み込み失敗でIOError発生
rescue ArgumentError => e
  puts "ユーザー入力に問題があります: #{e.message}"
  # ログを残す、フォームに戻すなどの対応
rescue IOError => e
  puts "ファイル読み込みに失敗しました: #{e.message}"
  # エラー内容を通知して別ファイルを読みにいく、などの対応
rescue => e
  puts "想定外のエラーが起きました: #{e.message}"
end

上記のように細かく分けることで、処理の分岐が整理しやすくなり、メンテナンスもしやすくなります。

rescueとensure・retryの関係

rescueと組み合わせてよく使われるキーワードとして、ensureretryがあります。
これらを活用すると、さらに高度なエラーハンドリングが行えます。

ensureで後処理を保証する

ensureは、begin ... rescue ... ensure ... endという書き方で利用し、エラーが起きても起きなくても必ず実行したい処理を記述する部分です。
たとえば、ファイルやネットワーク接続を開いたら、最後に必ずクローズしたいですよね。
そういうときに使われるのがensureです。

file = nil
begin
  file = File.open("data.txt", "r")
  content = file.read
  puts "ファイル内容: #{content}"
rescue => e
  puts "ファイル操作中にエラーが発生しました: #{e.message}"
ensure
  file.close if file
end

ファイルをopenした後は、途中でエラーがあろうとなかろうと、file.close if fileが呼ばれます。
実務ではデータベース接続や外部APIコネクションを開いた後、必ず切断処理を行うケースも同様にensureを使うことが多いです。

retryで再試行する

ネットワーク通信や、外部システムへのリクエストでは、一時的な障害で失敗する場合も少なくありません。
そんなときに、一度の失敗で諦めずに再試行したい場合はretryを利用します。

require "net/http"

retries = 0
begin
  response = Net::HTTP.get(URI("https://example.com/api/data"))
  puts "データ取得に成功: #{response}"
rescue => e
  retries += 1
  if retries < 3
    puts "再試行します...(#{retries}回目)"
    retry
  else
    puts "通信に失敗しました: #{e.message}"
  end
end

ここでは通信エラーが起きた場合に最大2回まで再試行し、3回目も失敗したらエラーを表示して終了する例です。
本番環境では、一度の通信エラーが偶発的に起きる場合があるため、こういったリトライの仕組みを作り込むことで、システムの安定性を高めることにつながります。

再試行を入れるときは、連続リトライによる負荷増大に注意してください。実務ではリトライ間隔をあける「指数バックオフ方式」などを導入するケースもあります。

rescueをメソッド内部で使う方法

Rubyでは、メソッド定義の中でもrescueを使うことができます。
以下のようにメソッドの定義とrescueを組み合わせると、メソッド内のエラーを外部に伝えないよう制御することができます。

def safe_division(a, b)
  a / b
rescue => e
  puts "エラーが発生しました: #{e.message}"
  nil
end

result = safe_division(10, 0)
puts "計算結果: #{result}"  # ここではnilが返る

ゼロ除算をしようとしてエラー(ZeroDivisionError)が起きた場合、rescueで捕捉してnilを返すことでエラーを上位に伝播させない形にしています。
実務ではメソッド単位でエラーハンドリングの方針を決めることが多いため、こうした書き方を覚えておくと便利です。

メソッド内部での例外再スロー

一方、メソッド内で一旦rescueしたあと、改めて別のエラーを投げ直す(再スローする)技法もあります。

def read_data(file_path)
  File.read(file_path)
rescue => e
  # ログや通知処理を行う
  puts "ファイル読み込み失敗: #{e.message}"
  # 必要なら再度エラーを投げ直す
  raise
end

begin
  content = read_data("data.txt")
  puts "読み込んだ内容: #{content}"
rescue => e
  puts "呼び出し元でさらに処理を続行できません: #{e.message}"
end

こうすると、メソッド内部でログを出力するだけでなく、呼び出し元にもエラーを伝えることができます。
大きなプログラムでは、メソッドの段階でエラーを握りつぶすのか、上位に伝播させるのかを設計として決めておくと見通しが良くなります。

rescue修飾子(inline rescue)の利用

Rubyには、 rescue修飾子 (inline rescue)と呼ばれる書き方も存在します。
これを使うと、わざわざbegin ... endで囲まなくても、単一行で簡易的なエラーハンドリングが可能です。

value = Integer("123abc") rescue 0
puts value  # "123abc" を整数変換できないので、失敗した場合は0を代入

ここでは、Integer("123abc")ArgumentErrorを起こした場合に0を代入するという動きになります。
しかし、この書き方は可読性にやや欠ける場合もあるため、本格的な開発ではあまり多用されません。
単純な一時的処理には便利ですが、複数のエラーをハンドリングしたり、後処理を行うには不向きです。

実務ではどんな場面で使うか

テストコードやスクリプトレベルで、少しの失敗なら無視してデフォルト値を返す、といった簡易的なケースで利用することがあります。
ただし、読みやすさやメンテナンスのしやすさを考えると、begin...rescueブロックをしっかり書いた方が後々の混乱を防ぎやすいでしょう。

大規模開発でのエラー処理設計のポイント

Rubyのrescueを使いこなす上で、実務ではさらに設計面を考慮します。
たとえば、どの層(メソッド、クラス、コントローラなど)でどの種類の例外をキャッチして、どのように扱うかを整理しておく必要があります。

アプリケーション全体のエラー処理方針

どのようなエラーはすぐにユーザーに伝え、どのようなエラーはリトライやリカバリを試みるか。

ログの出力

どのタイミングで何をログに残すのか。あまりに大量のログを残すと可読性が下がるため、レベル分け(info、warn、errorなど)をして管理する。

例外クラスのカスタマイズ

標準的な例外クラスに加えて、自分たちのアプリケーション専用の例外クラスを作ることで、rescueの分岐を分かりやすくできる場合がある。

実務では特に、大規模なWebサービスや長期間運用するシステムにおいて、例外処理が複雑になりがちです。
エラーの発生を防ぐこと自体が理想ですが、想定外の事態はどうしても起きてしまうため、rescueの使い方をちゃんと設計しておくことが大切です。

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

たとえば、以下のように独自例外クラスを作って、それだけをキャッチするという方法があります。

class MyAppError < StandardError; end
class InvalidInputError < MyAppError; end
class NetworkTimeoutError < MyAppError; end

def process_data(input)
  raise InvalidInputError, "入力が不正です" if input.nil? || input.empty?
  # 処理...
end

def fetch_external_data
  # タイムアウトしたらNetworkTimeoutErrorを発生させる
  # ...
end

begin
  process_data(params[:input])
  fetch_external_data
rescue InvalidInputError => e
  puts "入力エラー: #{e.message}"
rescue NetworkTimeoutError => e
  puts "ネットワークタイムアウト: #{e.message}"
rescue => e
  puts "その他のエラーが発生: #{e.message}"
end

こうすることで、どのパートでどんな種類のエラーが発生し得るのかをコードから把握しやすくなり、保守もしやすくなることが多いです。

まとめ

Rubyのrescueは、プログラム中で発生するさまざまなエラーを柔軟に制御するための必須機能といえるでしょう。
ファイル操作やネットワーク通信、ユーザー入力の検証など、実務ではエラーを起こしやすい箇所が多いため、rescueを上手に使ってプログラムの安全性を高めることが大切です。

  • 基本構文はbegin...rescue...endで、エラーが起きたときだけrescueブロックが実行される
  • StandardErrorをはじめ、特定の例外クラスを指定して複数のエラーをキャッチできる
  • ensureを使えば、エラーの有無にかかわらず必ず実行したい後処理を追加できる
  • retryを使えば、一時的な失敗に対して再試行する仕組みを作れる
  • メソッド内部inline rescueなど、状況に応じて書き分ける方法も用意されている
  • 大規模開発では、例外クラスの使い分けやエラー処理の設計が重要になる

エラー処理は地味に思えるかもしれませんが、実際のプロダクトでは大きな安心感をもたらす重要な要素です。
Rubyで開発する際は、rescueの基本をしっかり押さえて、実務に役立ててみてください。

Rubyをマスターしよう

この記事で学んだRubyの知識をさらに伸ばしませんか?
Udemyには、現場ですぐ使えるスキルを身につけられる実践的な講座が揃っています。