web-dev-qa-db-ja.com

Rubyの例外メッセージに情報を追加するにはどうすればよいですか?

ルビのクラスを変更せずに例外メッセージに情報を追加するにはどうすればよいですか?

私が現在使用しているアプローチは

strings.each_with_index do |string, i|
  begin
    do_risky_operation(string)
  rescue
    raise $!.class, "Problem with string number #{i}: #{$!}"
  end
end

理想的には、バックトレースも保存したいと思います。

もっと良い方法はありますか?

57
Andrew Grimm

例外を再発生させてメッセージを変更するには、例外クラスとそのバックトレースを保持しながら、次のようにします。

strings.each_with_index do |string, i|
  begin
    do_risky_operation(string)
  rescue Exception => e
    raise $!, "Problem with string number #{i}: #{$!}", $!.backtrace
  end
end

どちらが得られます:

# RuntimeError: Problem with string number 0: Original error message here
#     backtrace...
94
BoosterStage

それはそれほど良くはありませんが、新しいメッセージで例外を再発生させることができます:

raise $!, "Problem with string number #{i}: #{$!}"

exceptionメソッドを使用して、変更された例外オブジェクトを自分で取得することもできます。

new_exception = $!.exception "Problem with string number #{i}: #{$!}"
raise new_exception
17
Chuck

ここに別の方法があります:

class Exception
  def with_extra_message extra
    exception "#{message} - #{extra}"
  end
end

begin
  1/0
rescue => e
  raise e.with_extra_message "you fool"
end

# raises an exception "ZeroDivisionError: divided by 0 - you fool" with original backtrace

(内部でexceptionメソッドを使用するように改訂。@ Chuckに感謝)

6
AlexChaffee

私のアプローチは、エラーのextendメソッドを拡張する匿名モジュールでrescuemessagedエラーを処理することです。

def make_extended_message(msg)
    Module.new do
      @@msg = msg
      def message
        super + @@msg
      end
    end
end

begin
  begin
      raise "this is a test"
  rescue
      raise($!.extend(make_extended_message(" that has been extended")))
  end
rescue
    puts $! # just says "this is a test"
    puts $!.message # says extended message
end

こうすることで、例外の他の情報(つまり、backtrace)を上書きしません。

4
Mark Rushakoff

Ryan Heneise's の回答が受け入れられるべきだと私は投票しました。

これは複雑なアプリケーションの一般的な問題であり、元のバックトレースを維持することは非常に重要であることが多いため、ErrorHandlingヘルパーモジュールにユーティリティメソッドがあります。

私たちが発見した問題の1つは、システムがめちゃくちゃな状態のときに、より意味のあるメッセージを生成しようとすると、例外ハンドラー自体の内部で例外が生成され、次のようにユーティリティ関数が強化されるということです。

def raise_with_new_message(*args)
  ex = args.first.kind_of?(Exception) ? args.shift : $!
  msg = begin
    sprintf args.shift, *args
  rescue Exception => e
    "internal error modifying exception message for #{ex}: #{e}"
  end
  raise ex, msg, ex.backtrace
end

うまくいくとき

begin
  1/0
rescue => e
  raise_with_new_message "error dividing %d by %d: %s", 1, 0, e
end

うまく変更されたメッセージが表示されます

ZeroDivisionError: error dividing 1 by 0: divided by 0
    from (irb):19:in `/'
    from (irb):19
    from /Users/sim/.rvm/rubies/Ruby-2.0.0-p247/bin/irb:16:in `<main>'

物事がうまくいかないとき

begin
  1/0
rescue => e
  # Oops, not passing enough arguments here...
  raise_with_new_message "error dividing %d by %d: %s", e
end

全体像を見失うことはありません

ZeroDivisionError: internal error modifying exception message for divided by 0: can't convert ZeroDivisionError into Integer
    from (irb):25:in `/'
    from (irb):25
    from /Users/sim/.rvm/rubies/Ruby-2.0.0-p247/bin/irb:16:in `<main>'
2
Sim

私はこのパーティーに6年遅れていることを認識していますが、... Ruby今週までのエラー処理を理解し、この質問に遭遇しました。回答は有用ですが、 -このスレッドの将来の読者に役立つかもしれない明白な(そして文書化されていない)振る舞いすべてのコードはRuby v2.3.1の下で実行されました。

@アンドリュー・グリムが尋ねる

ルビのクラスを変更せずに例外メッセージに情報を追加するにはどうすればよいですか?

次にサンプルコードを提供します:

raise $!.class, "Problem with string number #{i}: #{$!}"

これは元のエラーインスタンスオブジェクトに情報を追加しないことを指摘するのは重要だと思いますが、代わりに同じクラスのNEWエラーオブジェクトを発生させます。

@BoosterStageさんのコメント

例外を再発生させてメッセージを変更するには...

しかし、再び、提供されたコード

raise $!, "Problem with string number #{i}: #{$!}", $!.backtrace

$!によって参照されるエラークラスの新しいインスタンスが発生しますが、$!とまったく同じインスタンスではありません

@Andrew Grimmのコードと@BoosterStageの例の違いは、最初のケースでは#raiseの最初の引数がClassであるのに対し、2番目のケースでは一部の(おそらく)StandardErrorのインスタンスであることです。 Kernel#raise のドキュメントには次のように記載されているため、違いは重要です。

単一のString引数を指定すると、文字列をメッセージとしてRuntimeErrorが発生します。それ以外の場合、最初のパラメーターはExceptionクラス(または、例外メッセージが送信されたときにExceptionオブジェクトを返すオブジェクト)の名前である必要があります。

引数が1つだけ指定され、それがエラーオブジェクトインスタンスである場合、そのオブジェクトはraised[〜#〜] if [〜#〜 ]そのオブジェクトの#exceptionメソッドは Exception#exception(string) で定義されたデフォルトの動作を継承または実装します:

引数がない場合、または引数がレシーバーと同じ場合は、レシーバーを返します。それ以外の場合は、レシーバーと同じクラスの新しい例外オブジェクトを作成しますが、メッセージはstring.to_strと同じです。

多くが推測するように:

...
catch StandardError => e
  raise $!
...

$!が参照するのと同じエラーを発生させます。

...
catch StandardError => e
  raise
...

しかし、おそらく考えられる理由ではありません。この場合、raiseの呼び出しは[〜#〜] not [〜#〜]であり、$!でオブジェクトを発生させるだけです。$!.exception(nil)、これはたまたま$!です。

この動作を明確にするために、このおもちゃのコードを検討してください:

      class TestError < StandardError
        def initialize(message=nil)
          puts 'initialize'
          super
        end
        def exception(message=nil)
          puts 'exception'
          return self if message.nil? || message == self
          super
        end
      end

それを実行する(これは、上記で引用した@Andrew Grimmのサンプルと同じです):

2.3.1 :071 > begin ; raise TestError, 'message' ; rescue => e ; puts e ; end

結果は:

initialize
message

したがって、TestErrorはinitialized、rescuedであり、メッセージが出力されました。ここまでは順調ですね。 2番目のテスト(上記で引用した@BoosterStageのサンプルに類似):

2.3.1 :073 > begin ; raise TestError.new('foo'), 'bar' ; rescue => e ; puts e ; end

やや驚くべき結果:

initialize
exception
bar

つまり、TestErrorinitializedと 'foo'でしたが、#raiseは最初の引数(TestErrorのインスタンス)で#exceptionを呼び出し、 'bar'のメッセージで渡してTestErrorの2番目のインスタンスを作成しました。これが最終的に発生するものです

TIL。

また、@ Simのように、元のバックトレースコンテキストを保持することについてveryが心配ですが、彼のraise_with_new_messageのようなカスタムエラーハンドラーを実装する代わりに、RubyのException#causeは、必要なときにいつでも利用できます:エラーをキャッチするには、ドメイン固有のエラーにラップしてからthatエラーを発生させますが、ドメイン固有のエラーが発生したときに#causeを介して元のバックトレースを引き続き利用できます。

このすべてのポイントは、@ Andrew Grimmのように、より多くのコンテキストでエラーを発生させたいということです。具体的には、多くのネットワーク関連の障害モードを持つ可能性があるアプリの特定のポイントからのみドメイン固有のエラーを発生させたいと考えています。次に、アプリの最上位レベルでドメインエラーを処理するようにエラーレポートを作成できます。「根本原因」に到達するまで#causeを再帰的に呼び出すことで、ロギング/レポートに必要なすべてのコンテキストを取得できます。

私はこのようなものを使用します:

class BaseDomainError < StandardError
  attr_reader :extra
  def initialize(message = nil, extra = nil)
    super(message)
    @extra = extra
  end
end
class ServerDomainError < BaseDomainError; end

次に、ファラデーのようなものを使用してリモートRESTサービスを呼び出している場合、すべての考えられるエラーをドメイン固有のエラーにラップし、追加情報を渡すことができます(これはオリジナルであると思います)このスレッドの質問):

class ServiceX
  def initialize(foo)
    @foo = foo
  end
  def get_data(args)
    begin
      # This method is not defined and calling it will raise an error
      make_network_call_to_service_x(args)
    rescue StandardError => e
      raise ServerDomainError.new('error calling service x', binding)
    end
  end
end

ええ、そうです。extraの情報を現在のbindingに設定して、ServerDomainErrorがインスタンス化/発生されたときに定義されたすべてのローカル変数を取得できることを文字通り理解しました。このテストコード:

begin
  ServiceX.new(:bar).get_data(a: 1, b: 2)
rescue
  puts $!.extra.receiver
  puts $!.extra.local_variables.join(', ')
  puts $!.extra.local_variable_get(:args)
  puts $!.extra.local_variable_get(:e)
  puts eval('self.instance_variables', $!.extra)
  puts eval('self.instance_variable_get(:@foo)', $!.extra)
end

出力されます:

exception
#<ServiceX:0x00007f9b10c9ef48>
args, e
{:a=>1, :b=>2}
undefined method `make_network_call_to_service_x' for #<ServiceX:0x00007f9b10c9ef48 @foo=:bar>
@foo
bar

Rails ServiceXを呼び出すコントローラーは、ServiceXが Faraday (またはgRPC、またはその他)を使用していることを特に知る必要はありません。呼び出しとハンドルを行うだけです。 BaseDomainError。ここでも、ロギングの目的で、最上位レベルの単一のハンドラーがキャッチされたエラーのすべての#causesを再帰的に記録できます。また、エラーチェーン内のBaseDomainErrorインスタンスについては、extra値をログに記録できます。カプセル化されたbinding(s)。

このツアーが私にとっても他の人にとっても同様に役立つことを願っています。私は多くのことを学びました。

更新: Skiptrace は、バインディングをRubyエラーに追加するようです。

また、Exception#exceptionの実装がオブジェクトをどのようにcloneするか(インスタンス変数をコピーする)については、 this other post を参照してください。

2
Lemon Cat

これが私がやったことです:

Exception.class_eval do
  def prepend_message(message)
    mod = Module.new do
      define_method :to_s do
        message + super()
      end
    end
    self.extend mod
  end

  def append_message(message)
    mod = Module.new do
      define_method :to_s do
        super() + message
      end
    end
    self.extend mod
  end
end

例:

strings = %w[a b c]
strings.each_with_index do |string, i|
  begin
    do_risky_operation(string)
  rescue
    raise $!.prepend_message "Problem with string number #{i}:"
  end
end
=> NoMethodError: Problem with string number 0:undefined method `do_risky_operation' for main:Object

そして:

pry(main)> exception = 0/0 rescue $!
=> #<ZeroDivisionError: divided by 0>
pry(main)> exception = exception.append_message('. With additional info!')
=> #<ZeroDivisionError: divided by 0. With additional info!>
pry(main)> exception.message
=> "divided by 0. With additional info!"
pry(main)> exception.to_s
=> "divided by 0. With additional info!"
pry(main)> exception.inspect
=> "#<ZeroDivisionError: divided by 0. With additional info!>"

これは Mark Rushakoff の答えに似ていますが、次のようになります。

  1. デフォルトではmessageは単にto_sとして定義されているため、messageではなくto_sをオーバーライドします(少なくともRuby 2.0および2.2でテストしたところ)。
  2. 呼び出し側に追加の手順を実行させる代わりに、extendを呼び出します。
  3. define_methodとクロージャを使用して、ローカル変数messageを参照できるようにします。クラスvariable @@messageを使用しようとすると、「警告:トップレベルからのクラス変数へのアクセス」という警告が表示されます(これを参照 question : "classキーワードでクラスを作成していないため、クラス変数は、[匿名モジュール]ではなくObjectに設定されています ")

特徴:

  • 使いやすい
  • (クラスの新しいインスタンスを作成するのではなく)同じオブジェクトを再利用するため、オブジェクトID、クラス、バックトレースなどが保持されます
  • to_smessageinspectはすべて適切に応答します
  • すでに変数に格納されている例外を使用して使用できます。何も再レイズする必要はありません(レイズするためにバックトレースを渡すことを含むソリューション:raise $!, …, $!.backtrace)。例外がロギングメソッドに渡されたため、これは私にとって重要であり、自分で救ったものではありませんでした。
0
Tyler Rick

別のアプローチは、例外に関するコンテキスト(追加情報)をstringとしてではなくhashとして追加することです。

チェックアウト このプルリクエスト ここで、いくつかの新しいメソッドを追加して、次のように例外にコンテキスト情報を簡単に追加できるようにすることを提案しました。

begin
  …
  User.find_each do |user|
    reraise_with_context(user: user) do
      send_reminder_email(user)
    end
  end
  …

rescue
  # $!.context[:user], etc. is available here
  report_error $!, $!.context
end

あるいは:

User.find_each.reraise_with_context do |user|
  send_reminder_email(user)
end

このアプローチの良い点は、非常に簡潔な方法で追加情報を追加できることです。また、元の例外をラップするための新しい例外クラスを定義する必要もありません。

私が@Lemon Catの answer を好きなだけ多くの理由で、それは確かにいくつかのケースに適している場合、あなたが実際にやろうとしているのは、元の例外に関する追加情報を添付することだと思います新しいラッパー例外を作成する(および別の間接層を追加する)よりも、それを関連する例外に直接アタッチすることをお勧めします。

もう一つの例:

class ServiceX
  def get_data(args)
    reraise_with_context(StandardError, binding: binding, service: self.class, callee: __callee__) do
      # This method is not defined and calling it will raise an error
      make_network_call_to_service_x(args)
    end
  end
end

このアプローチの欠点は、exception.contextで利用可能な情報を実際にuseするようにエラー処理を更新する必要があることです。しかし、ルート例外を取得するためにcauseを再帰的に呼び出すには、anywayを実行する必要があります。

0
Tyler Rick