インライン rescue を使う時の注意点


Ruby の rescue をインラインで, つまりは statement modifier として使う時の注意点を紹介します.

Ruby のバージョンは 2.5.1 になります.

インライン rescue は, 節 rescue の速記

rescue を次のように statement modifier として使うと:

raise rescue puts 'RuntimeError occured'

raiseRuntime Error を発生させるので, RuntimeErrorStandardError のサブクラスなので, StandardError とそのサブクラスの全ての例外を rescue はキャッチするので, 次のような結果になります:

RuntimeError occured

つまりは次のような begin 文の節として使う場合の速記になります:

begin
  raise
rescue
  puts 'RuntimeError occured'
end

このように rescue を例外クラスを何も指定しないで使う場合, インラインの rescue の方がより簡潔に書くことができるためそのようなメリットがあるのですが, 思わぬ落とし穴があったりします.

インライン rescue の落とし穴

例えば次のような 2 分の 1 の確率で odd を返し, 2 分の 1 の確率で raise するメソッドを定義したとします:

def ood_or_even
  rand(2) == 0 ? 'odd' : raise
end

10.times do
  puts odd_or_even rescue puts 'even'
end

そのメソッドを 10.times のブロック内で呼び出して, 例外が発生しなかったら odd, 発生したら even を 10 回, 2 分の 1 の確率でそれぞれ表示すると一見すると思われますが, 実際の結果は次のようになります:

even
even
even
even
even
even
even
even
even
even

10 回中 10 回とも全て even です. rand(2) で例外が発生する確率は 2 分の 1 の筈なのにそんなことってあり得るのと思われるかも知れません.

もしかしたら 2 の 10 乗である 1024 分の 1 の確率を引いたのかと, 思われるかも知れませんが, 10.times100.times としても, はたまた 1000.times としても表示されるのは全て even になります:

100.times.map { odd_or_even rescue 'even' }.all? 'even'  # => true
1000.times.map { odd_or_even rescue 'even' }.all? 'even' # => true

つまりは確率の問題ではありません.

ここに rescue をインラインで使った時の落とし穴があります.

rescue は最初の方でも書きましたが, StandardError とそのサブクラスの全ての例外をキャッチします.

なので次の行の:

puts odd_or_even rescue puts 'even'

rescue がキャッチする例外は必ずしも RuntimeError だけではないのです.

実は RuntimeError 以外の例外が 100% の確率で発生していたので, そのように even が 10 回中 10 回とも表示されたのです.

RuntimeError 以外の例外とは一体何なのかを見つけるために先ほどのコードを次のように変更してみます:

10.times do
  puts odd_or_even rescue p $!
end

$!raise によって発生した例外情報が入ってします.

そして実行すると, 次のように表示されます:

#<NameError: undefined local variable or method `odd_or_even' for main:Object
Did you mean?  ood_or_even>
#<NameError: undefined local variable or method `odd_or_even' for main:Object
Did you mean?  ood_or_even>
#<NameError: undefined local variable or method `odd_or_even' for main:Object
Did you mean?  ood_or_even>
#<NameError: undefined local variable or method `odd_or_even' for main:Object
Did you mean?  ood_or_even>
#<NameError: undefined local variable or method `odd_or_even' for main:Object
Did you mean?  ood_or_even>
#<NameError: undefined local variable or method `odd_or_even' for main:Object
Did you mean?  ood_or_even>
#<NameError: undefined local variable or method `odd_or_even' for main:Object
Did you mean?  ood_or_even>
#<NameError: undefined local variable or method `odd_or_even' for main:Object
Did you mean?  ood_or_even>
#<NameError: undefined local variable or method `odd_or_even' for main:Object
Did you mean?  ood_or_even>
#<NameError: undefined local variable or method `odd_or_even' for main:Object
Did you mean?  ood_or_even>

このように正体は NameError でした.

メッセージを見ると, odd_or_even というメソッドが定義されていないという事がわかります.

先ほどのコードを見ていただけると odd_or_even というメソッドを定義したつもりが ood_or_even となってしまってします.

この NameErrorRuntimeError の前に発生していたので, 先ほどのような全て even という結果になりました.

なのでその ood_or_even メソッドの名前を odd_or_even と直して定義し直すと, ちゃんと想定通りの結果が表示されます:

odd
even
odd
even
even
odd
even
even
even
even

特定の例外を拾う場合は rescue を節として使う

このように rescue をインラインで使ってしまうと, 想定している例外も拾ってくれますが, 想定していない例外も同様に拾ってしまうので, 先ほどの 10.times のブロック内のコードは RuntimeError という特定の例外を拾うために次のように書くといいです:

10.times do
  puts odd_or_even
rescue RuntimeError
  puts 'even'
rescue
  p $!
end

実行すると NameError が発生したと知る事ができます:

#<NameError: undefined local variable or method `odd_or_even' for main:Object
Did you mean?  ood_or_even>

次の 2 行は, 例外情報をわかりやすく表示させるために付け足したものなので, 削除しても全く問題ありません:

rescue
  p $!

このように特定の例外クラスを raise してその特定の例外を拾う必要がある場合は, インラインの rescue ではなく, 節としての rescue を使われた方がいいです.

まとめ

Ruby コミッタを務められている, 卜部昌平さんは rescue 修飾語句 (インライン rescue のこと) は beginend の節として使う rescue と比べて使いやすすぎると感じられていて, その短さ故にその修飾語句スタイルは不適切に使われがちと述べられています.1

そして rescue 修飾語句を使う場合は, rescue の左側は式で, 右側は nilfalse のような副作用がない使い方に限定するコーディングスタイルを個人的に提案されています.

なのでそのようなコーディングスタイルで書けないような rescue は, 文の節として, キャッチする特定の例外クラス付きで定義した方がいいのでしょう.