LoginSignup
6
2

More than 3 years have passed since last update.

Ruby で例外を発生させない方法

Posted at

例外に悩まされたことはありませんか?

some_exceptional_library.rb
module SomeExceptionalLibrary

  class TrivialException < StandardError; end

  # 雑にランダムで例外を飛ばすように書いているけど、
  # 実際は様々な条件が絡み合ってごく稀に例外が発生してしまうような感じ。
  def self.some_brilliant_method
    if rand < 0.01
      raise TrivialException.new 
    else
      42
    end
  end
end
my_app.rb
require "./some_exceptional_library"

puts SomeExceptionalLibrary.some_brilliant_method

このライブラリはとても素晴らしいライブラリなのですが、時々、例外を発生させてしまいます。

$ ruby my_app.rb
42
$ ruby my_app.rb
42
$ ruby my_app.rb
Traceback (most recent call last):
        1: from my_app.rb:3:in `<main>'
/Users/cedretaber/path/to/sample/some_exceptional_library.rb:7:in `some_brilliant_method': SomeExceptionalLibrary::TrivialException (SomeExceptionalLibrary::TrivialException)

例外に対処するのは大変ですよね。
かといって、 例外を握りつぶす のも良くないことと言われています。

では、どうすれば良いのでしょうか?

そうですね。 例外を投げられないようにしてやればいい のですね。

例外なんて投げさせない

こんなコード片を挿入してやりましょう。

my_app.rb
require "./some_exceptional_library"

module Kernel
  def raise *_args1, **_args2
    puts "All right. No problem!"
  end

  alias fail raise
end

puts SomeExceptionalLibrary.some_brilliant_method

これで例外は発生しません。

$ ruby my_app.rb
42
$ ruby my_app.rb
42
$ ruby my_app.rb
All right. No problem!

お前は何を言っているんだ?

(本気にする人はいないと思いますが)ネタです。プロダクトとかでは絶対にやらないでください。

さて、どうしてこんなことができるのでしょう。

Ruby の予約語一覧を見てみましょう。
nextreturn といった制御構造、 raise とセットで使うであろう rescue などは入っていますが、 なんと raise は予約語ではありません

じゃあ例外を投げる時に使っている raise とは何なのかと言うと、上のコードを見れば一目瞭然で、これは Kernel モジュールに定義されたメソッドです。そう、単なるメソッドなのです。

そしてご存知の通り、 Ruby はオープンクラスの力を用いることで、組み込みメソッドの挙動すら変更してしまえます。つまり、 raise の動きは変えることができるのです。

なので、引数を全て無視するメソッドに書き換えてやればこの通り、例外の発生自体を封じ込めることができます。
やった! 我々は例外から解放された!

これ、役に立つの?

たぶん立ちません。

次のようなコードに変えてみましょう。

my_app.rb
require "./some_exceptional_library"

module Kernel
  def raise *_args1, **_args2
    puts "All right. No problem!"
  end

  alias fail raise
end

puts SomeExceptionalLibrary.some_brilliant_method + 1 # ここ
$ ruby my_app.rb
43
$ ruby my_app.rb
43
$ ruby my_app.rb
All right. No problem!
Traceback (most recent call last):
my_app.rb:11:in `<main>': undefined method `+' for nil:NilClass (NoMethodError)

はい、封じ込めていたはずの例外が逃げ出していますね。

どういうことかというと、 Kernel#raise を書き換えて防げるのはあくまで「 Ruby コード中の raise メソッドで投げている例外」で、 C レベルで発生する例外までは防げないのです。まぁ当然ですね。
ここの NoMethodError は Object#method_missing が発生させている例外ですが、この処理は C で書かれており、 rb_exc_raise という例外を発生させる関数を直接呼び出しています。なので、いくら Kernel のメソッドを上書きしても駄目なのですね。

「じゃあ Object#method_missing を書き換えればいいじゃん」となりそうですが、その他にも例外を発生させる組み込みメソッドはいくらでもありますし(例えば零除算で発生する ZeroDivisionError )、それらにいちいち対応するという生産性のない作業に時間を費やすよりは、例外発生の原因を突き止めて問題を解消する方がよほど有意義でしょう。

例外から逃げてはいけません。

例外と式と文

上で述べたように 例外を握りつぶす という言葉があります。
次のようなコードを書くことを言います。


begin
  SomeExceptionalLibrary.some_brilliant_method # 例外が飛ぶかも
rescue
end

投げられた例外を捉えて、 特に何もせず処理を終える ことで、例外を呼び出し元にエスカレーションせずに無視するテクニックです。

テクニックと書きましたが、基本的に例外の握りつぶしは禁じ手と言うか、やらない方が良いことと言われています。私もそう思います。

まぁ、「だったら例外自体を発生させなきゃ良いんだろう」というわけではないのですね。

というか、どこで失敗したのか丁寧に教えてくれる例外はとても尊いものです。例外は決して敵ではありません!

なおこの記事は、Ruby の『文』は return, retry, redo, next, break, alias だけというのを読んで、「あれ raise は?」と疑問に思い調べてみた結果報告になります。

6
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2