ルビのクラスを変更せずに例外メッセージに情報を追加するにはどうすればよいですか?
私が現在使用しているアプローチは
strings.each_with_index do |string, i|
begin
do_risky_operation(string)
rescue
raise $!.class, "Problem with string number #{i}: #{$!}"
end
end
理想的には、バックトレースも保存したいと思います。
もっと良い方法はありますか?
例外を再発生させてメッセージを変更するには、例外クラスとそのバックトレースを保持しながら、次のようにします。
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...
それはそれほど良くはありませんが、新しいメッセージで例外を再発生させることができます:
raise $!, "Problem with string number #{i}: #{$!}"
exception
メソッドを使用して、変更された例外オブジェクトを自分で取得することもできます。
new_exception = $!.exception "Problem with string number #{i}: #{$!}"
raise new_exception
ここに別の方法があります:
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に感謝)
私のアプローチは、エラーのextend
メソッドを拡張する匿名モジュールでrescue
message
dエラーを処理することです。
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
)を上書きしません。
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>'
私はこのパーティーに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つだけ指定され、それがエラーオブジェクトインスタンスである場合、そのオブジェクトはraise
d[〜#〜] 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はinitialize
d、rescue
dであり、メッセージが出力されました。ここまでは順調ですね。 2番目のテスト(上記で引用した@BoosterStageのサンプルに類似):
2.3.1 :073 > begin ; raise TestError.new('foo'), 'bar' ; rescue => e ; puts e ; end
やや驚くべき結果:
initialize
exception
bar
つまり、TestError
はinitialize
dと '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
。ここでも、ロギングの目的で、最上位レベルの単一のハンドラーがキャッチされたエラーのすべての#cause
sを再帰的に記録できます。また、エラーチェーン内のBaseDomainError
インスタンスについては、extra
値をログに記録できます。カプセル化されたbinding
(s)。
このツアーが私にとっても他の人にとっても同様に役立つことを願っています。私は多くのことを学びました。
更新: Skiptrace は、バインディングをRubyエラーに追加するようです。
また、Exception#exception
の実装がオブジェクトをどのようにcloneするか(インスタンス変数をコピーする)については、 this other post を参照してください。
これが私がやったことです:
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 の答えに似ていますが、次のようになります。
message
は単にto_s
として定義されているため、message
ではなくto_s
をオーバーライドします(少なくともRuby 2.0および2.2でテストしたところ)。extend
を呼び出します。define_method
とクロージャを使用して、ローカル変数message
を参照できるようにします。クラスvariable @@message
を使用しようとすると、「警告:トップレベルからのクラス変数へのアクセス」という警告が表示されます(これを参照 question : "classキーワードでクラスを作成していないため、クラス変数は、[匿名モジュール]ではなくObject
に設定されています ")特徴:
to_s
、message
、inspect
はすべて適切に応答しますraise $!, …, $!.backtrace
)。例外がロギングメソッドに渡されたため、これは私にとって重要であり、自分で救ったものではありませんでした。別のアプローチは、例外に関するコンテキスト(追加情報)を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を実行する必要があります。