如何在不更改ruby中的类的情况下向异常消息添加信息?
我目前使用的方法是
strings.each_with_index do |string, i|
begin
do_risky_operation(string)
rescue
raise $!.class, "Problem with string number #{i}: #{$!}"
end
end
理想情况下,我还想保留回溯。
有更好的方法吗?
答案 0 :(得分:93)
要重新加载异常并修改消息,同时保留异常类及其回溯,只需执行以下操作:
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...
答案 1 :(得分:17)
这不是更好,但您可以通过新消息重新加注异常:
raise $!, "Problem with string number #{i}: #{$!}"
您还可以使用exception
方法自行获取修改后的异常对象:
new_exception = $!.exception "Problem with string number #{i}: #{$!}"
raise new_exception
答案 2 :(得分:6)
这是另一种方式:
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)
答案 3 :(得分:4)
我的方法是extend
使用匿名模块rescue
d错误扩展错误的message
方法:
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 :(得分:2)
我认为Ryan Heneise's回答应该是被接受的投票。
这是复杂应用程序中的常见问题,保留原始回溯通常非常重要,因此我们在ErrorHandling
帮助程序模块中有一个实用程序方法。
我们发现的一个问题是,当系统处于混乱状态时,有时会尝试生成更有意义的消息,这会导致在异常处理程序本身内部生成异常,这导致我们强化我们的实用程序功能,如下所示: / p>
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>'
答案 5 :(得分:2)
我意识到我参加这个聚会已经晚了6年,但是...我以为直到本周才了解Ruby错误处理,并遇到了这个问题。尽管答案是有用的,但存在一些非显而易见的(和未记录的)行为,可能对该线程的未来读者有用。所有代码都在ruby v2.3.1。下运行。
@安德鲁·格林(Andrew Grimm)问
如何在不更改ruby类的情况下向异常消息添加信息?
,然后提供示例代码:
raise $!.class, "Problem with string number #{i}: #{$!}"
我认为指出不要向原始错误实例对象添加信息,而是引发具有相同类的NEW错误对象是关键。
@BoosterStage说
要引发异常并修改消息...
但再次提供的代码
raise $!, "Problem with string number #{i}: #{$!}", $!.backtrace
将引发$!引用的任何错误类的新实例,但不是与$!完全相同。
@Andrew Grimm的代码与@BoosterStage的示例之间的区别在于,在第一种情况下,#raise
的第一个参数是Class
,而在第二种情况下,它是某些实例(大概)StandardError
。区别很重要,因为Kernel#raise的文档说:
使用单个String参数,将RuntimeError与字符串作为消息引发。否则,第一个参数应该是Exception类的名称(或发送异常消息时返回Exception对象的对象)。
如果仅给出一个参数且它是一个错误对象实例,则该对象将被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
因此,发生了initialize
d,rescue
d的TestError,并打印了其消息。到目前为止,一切都很好。第二个测试(类似于上面引用的@BoosterStage的示例):
2.3.1 :073 > begin ; raise TestError.new('foo'), 'bar' ; rescue => e ; puts e ; end
有些令人惊讶的结果:
initialize
exception
bar
因此TestError
被initialize
加上'foo',但随后#raise
在第一个参数(#exception
的实例)上调用了TestError
。并传递“ bar” 消息,以创建TestError
的第二个实例,该实例最终被引发。
TIL。
我也像@Sim一样,非常担心保留所有原始回溯上下文,但是Ruby的raise_with_new_message
并没有像他的Exception#cause
那样实现自定义错误处理程序,而是我的后背:每当我想捕获错误时,将其包装在特定于域的错误中,然后引发那个错误,我仍然可以通过特定于域的#cause
获得原始回溯出现错误。
所有这一切的重点是,就像@Andrew Grimm一样,我想在更多上下文中引发错误;具体来说,我只想从我的应用程序中可能引起许多与网络相关的故障模式的某些方面引发特定于域的错误。然后,可以进行我的错误报告来处理应用程序顶层的域错误,并且通过递归调用#cause
直到获得“根本原因”,我拥有了记录/报告所需的所有上下文。 / p>
我使用这样的东西:
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
现在,调用ServiceX的Rails控制器不需要特别知道ServiceX正在使用Faraday(或gRPC或其他任何东西),它只需进行调用并处理BaseDomainError
。再次:出于记录目的,单个顶级处理程序可以递归记录所有捕获到的错误的所有#cause
,对于错误链中的任何BaseDomainError
实例,它也可以记录{{1 }}值,可能包括从封装的extra
中提取的局部变量。
我希望这次旅行对其他人和我一样有用。我学到了很多东西。
更新:Skiptrace似乎将绑定添加到Ruby错误中。
此外,有关binding
的实现将如何克隆对象(复制实例变量)的信息,请参见this other post。
答案 6 :(得分:0)
这是我最终做的事情:
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的答案,但是:
to_s
而不是message
,因为默认情况下message
被定义为to_s
(至少在我测试过的Ruby 2.0和2.2中)extend
,而不是让调用者执行额外的步骤。define_method
和闭包,以便可以引用局部变量message
。当我尝试使用类variable @@message
时,它警告,&#34;警告:来自顶层的类变量访问&#34; (请参阅此question:&#34;由于您未使用class关键字创建类,因此您的类变量将设置在Object
上,而不是[您的匿名模块]&#34 ;)特点:
to_s
,message
和inspect
都做出了适当的回应raise $!, …, $!.backtrace
)。这对我很重要,因为异常传递给了我的日志记录方法,而不是我自己救出的东西。答案 7 :(得分:0)
另一种方法是将有关异常的上下文(额外信息)作为 hash 而不是作为 string 。
请检出this pull request,在这里我建议添加一些新方法,以使添加额外的上下文信息非常容易,例如:
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
中可能可用的信息。但是无论如何,您都必须这样做 ,以便递归调用cause
才能获得根本的激励。