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

时间:2010-05-13 00:36:37

标签: ruby exception exception-handling

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

我目前使用的方法是

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

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

有更好的方法吗?

8 个答案:

答案 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

因此TestErrorinitialize加上'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的答案,但是:

  1. 覆盖to_s而不是message,因为默认情况下message被定义为to_s(至少在我测试过的Ruby 2.0和2.2中)
  2. 为您调用extend,而不是让调用者执行额外的步骤。
  3. 使用define_method和闭包,以便可以引用局部变量message。当我尝试使用类variable @@message时,它警告,&#34;警告:来自顶层的类变量访问&#34; (请参阅此question:&#34;由于您未使用class关键字创建类,因此您的类变量将设置在Object上,而不是[您的匿名模块]&#34 ;)
  4. 特点:

    • 易于使用
    • 重用相同的对象(而不是创建类的新实例),因此保留了对象标识,类和回溯等内容。
    • to_smessageinspect都做出了适当的回应
    • 可以与已存储在变量中的异常一起使用;不要求你重新提出任何东西(比如涉及传递回溯的解决方案: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才能获得根本的激励。