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

How do I add information to an exception message in Ruby?

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

我目前使用的方法是

1
2
3
4
5
6
7
strings.each_with_index do |string, i|
  begin
    do_risky_operation(string)
  rescue
    raise $!.class,"Problem with string number #{i}: #{$!}"
  end
end

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

有更好的方法吗?


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

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

这将产生:

1
2
# RuntimeError: Problem with string number 0: Original error message here
#     backtrace...


这并不是更好,但是你可以用一条新的信息来重新判断例外:

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

您还可以使用exception方法自己获得一个修改过的异常对象:

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


另一种方法是:

1
2
3
4
5
6
7
8
9
10
11
12
13
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)


我的方法是使用匿名模块来扩展错误的message方法来处理extendrescued错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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)。


我投的票是瑞安·海涅斯的回答应该是被接受的。

这是复杂应用程序中的一个常见问题,保留原始回溯通常非常关键,因此我们的ErrorHandling助手模块中有一个实用方法可以解决这一问题。

我们发现的一个问题是,当系统处于混乱状态时,有时试图生成更有意义的消息会导致异常处理程序内部生成异常,这导致我们强化了实用程序功能,如下所示:

1
2
3
4
5
6
7
8
9
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

当一切顺利的时候

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

你得到一条修改得很好的信息

1
2
3
4
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>'

当情况恶化时

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

你仍然不会忘记大局

1
2
3
4
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>'


我最后做的是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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

实例:

1
2
3
4
5
6
7
8
9
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

还有:

1
2
3
4
5
6
7
8
9
10
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!>"

这与马克·拉沙科夫的回答类似,但:

  • 重写to_s,而不是message,因为默认情况下,message被定义为简单的to_s(至少在Ruby2.0和2.2中,我测试过它)。
  • 打电话给extend,而不是让打电话的人做额外的步骤。
  • 使用define_method和一个闭包,以便可以引用局部变量message。当我尝试使用一个类variable @@message时,它警告说:"警告:从顶层访问类变量"(参见这个问题:"由于您不是用class关键字创建类,所以您的类变量是在Object上设置的,而不是[您的匿名模块])
  • 特征:

    • 易于使用
    • 重用同一个对象(而不是创建类的新实例),因此保留对象标识、类和回溯等内容
    • to_smessageinspect都做出了适当的反应。
    • 可以与已存储在变量中的异常一起使用;不需要重新引发任何内容(例如,涉及传递backtrace来引发的解决方案:raise $!, …, $!.backtrace)。这对我很重要,因为异常被传递到我的日志记录方法中,而不是我自己挽救的东西。