如何在Ruby中向异常消息添加信息?

And*_*imm 55 ruby exception-handling exception

如何在不更改ruby中的类的情况下向异常消息添加信息?

我目前使用的方法是

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

理想情况下,我还想保留回溯.

有没有更好的办法?

Boo*_*age 95

要在保留异常类及其回溯的同时重新加载异常并修改消息,只需执行以下操作:

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

哪个会产生:

# RuntimeError: Problem with string number 0: Original error message here
#     backtrace...
Run Code Online (Sandbox Code Playgroud)

  • 在以"raise"开头的行上,是否有理由使用`$!`而不是`e`?他们是同一个对象. (5认同)
  • 请注意,抢救"异常"可能不是您想要的/sf/ask/703372141/. (3认同)
  • 还要注意`$!`是一个_global变量_指向运行Ruby程序时引发的最后一个异常.它可以在救援块期间被另一个线程中引发的另一个异常覆盖.这是喜欢使用`e`的一个原因. (3认同)
  • 你也可以使用`e2 = e.class.new"Foo:#{e}"`来创建一个相同类型的新异常,然后使用`e2.set_backtrace(e.backtrace)`来从原来做回溯例外. (2认同)

Chu*_*uck 17

它不是更好,但你可以用一条新消息重新加载异常:

raise $!, "Problem with string number #{i}: #{$!}"
Run Code Online (Sandbox Code Playgroud)

您还可以使用以下exception方法自行获取已修改的异常对象:

new_exception = $!.exception "Problem with string number #{i}: #{$!}"
raise new_exception
Run Code Online (Sandbox Code Playgroud)

  • @yar:不,这不会破坏原始邮件.这就是`#{$!}`插值的全部目的.如果你把它留下来,它会删除原始信息,就像你在Mark的方法中省略了对`super`的调用一样.我坦率地说我的方式更保守,因为它只是使用语言的预期异常重新提升机制,而Mark的解决方案涉及创建一个完整的模块并重新定义`message`方法以获得相同的效果. (2认同)

Lem*_*Cat 8

我意识到我参加这个聚会晚了 6 年,但是......我以为我直到本周才了解 Ruby 错误处理并遇到了这个问题。虽然答案很有用,但有一些不明显(和未记录)的行为可能对这个线程的未来读者有用。所有代码都在 ruby​​ v2.3.1 下运行。

@Andrew Grimm 问

如何在不更改 ruby​​ 中的类的情况下向异常消息添加信息?

然后提供示例代码:

raise $!.class, "Problem with string number #{i}: #{$!}"
Run Code Online (Sandbox Code Playgroud)

我认为重要的是指出这不会向原始错误实例对象添加信息,而是引发具有相同类的新错误对象。

@BoosterStage 说

要重新引发异常并修改消息...

但同样,提供的代码

raise $!, "Problem with string number #{i}: #{$!}", $!.backtrace
Run Code Online (Sandbox Code Playgroud)

将引发 $! 引用的任何错误类的新实例,但它不会与 $! 完全相同。

@Andrew Grimm 的代码和@BoosterStage 的示例之间的区别在于,#raise在第一种情况下Class,to的第一个参数是 a ,而在第二种情况下,它是 some (大概)的实例StandardError。差异很重要,因为Kernel#raise的文档说:

使用单个 String 参数,以字符串作为消息引发 RuntimeError。否则,第一个参数应该是 Exception 类的名称(或在发送异常消息时返回 Exception 对象的对象)。

如果只给出一个参数并且它是一个错误对象实例,那么raise 如果该对象的#exception方法继承或实现了Exception#exception(string) 中定义的默认行为,则该对象将是d

没有参数,或者如果参数与接收者相同,则返回接收者。否则,创建一个与接收者类相同的新异常对象,但消息等于 string.to_str。

正如许多人猜测的那样:

...
catch StandardError => e
  raise $!
...
Run Code Online (Sandbox Code Playgroud)

引发 $! 引用的相同错误,与简单调用相同:

...
catch StandardError => e
  raise
...
Run Code Online (Sandbox Code Playgroud)

但可能不是因为人们可能认为的原因。在这种情况下,调用raise只是提高了对象$!......它提出的结果$!.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
Run Code Online (Sandbox Code Playgroud)

运行它(这与我上面引用的@Andrew Grimm 的示例相同):

2.3.1 :071 > begin ; raise TestError, 'message' ; rescue => e ; puts e ; end
Run Code Online (Sandbox Code Playgroud)

结果是:

initialize
message
Run Code Online (Sandbox Code Playgroud)

所以一个 TestError 是initialized, rescued,并打印了它的消息。到现在为止还挺好。第二个测试(类似于上面引用的@BoosterStage 的示例):

initialize
message
Run Code Online (Sandbox Code Playgroud)

结果有些出人意料:

initialize
exception
bar
Run Code Online (Sandbox Code Playgroud)

所以 aTestErrorinitialize带有 'foo' 的 d,但随后#raise调用#exception了第一个参数( 的实例TestError)并传入了 'bar' 的消息以创建 的第二个实例TestError,这就是最终引发的

直到。

此外,像@Sim 一样,我非常关心保留任何原始的回溯上下文,但不是像他那样实现自定义错误处理程序,而是raise_with_new_messageRuby 支持Exception#cause我:每当我想捕获错误时,将其包装在特定于域的错误中,然后然后引发错误,我仍然可以通过#cause引发的特定于域的错误获得原始回溯。

这一切的重点是——就像@Andrew Grimm——我想用更多的上下文来提出错误;具体来说,我只想从我的应用程序中的某些点引发特定于域的错误,这些错误可能具有许多与网络相关的故障模式。然后我的错误报告可以用来处理我的应用程序顶层的域错误,并且我通过#cause递归调用获得日志/报告所需的所有上下文,直到我找到“根本原因”。

我使用这样的东西:

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

然后,如果我使用 Faraday 之类的东西来调用远程 REST 服务,我可以将所有可能的错误包装到特定于域的错误中并传入额外的信息(我认为这是该线程的原始问题):

initialize
exception
bar
Run Code Online (Sandbox Code Playgroud)

是的,没错:我刚刚意识到我可以将extra信息设置为当前,binding以获取在ServerDomainError实例化/引发时定义的所有本地变量。此测试代码:

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

将输出:

exception
#<ServiceX:0x00007f9b10c9ef48>
args, e
{:a=>1, :b=>2}
undefined method `make_network_call_to_service_x' for #<ServiceX:0x00007f9b10c9ef48 @foo=:bar>
@foo
bar
Run Code Online (Sandbox Code Playgroud)

现在,调用 ServiceX 的 Rails 控制器并不特别需要知道 ServiceX 正在使用Faraday(或 gRPC,或其他任何东西),它只需要调用并处理BaseDomainError. 再次:出于日志记录的目的,顶层的单个处理程序可以递归地记录#cause任何捕获错误的所有s,并且对于BaseDomainError错误链中的任何实例,它还可以记录extra值,可能包括从封装的binding(s )。

我希望这次旅行对其他人和我一样有用。我学到了很多。

更新:Skiptrace看起来像是将绑定添加到 Ruby 错误。

此外,请参阅本等岗位的信息有关的实施如何 Exception#exception克隆的对象(复制实例变量)。


Ale*_*fee 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
Run Code Online (Sandbox Code Playgroud)

(修改为在exception内部使用该方法,感谢@Chuck)