Как добавить информацию в сообщение об исключении в Ruby?


Как добавить информацию в сообщение об исключении без изменения его класса в ruby?

в настоящее время я использую подход

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

В идеале, я также хотел бы сохранить backtrace.

есть ли лучший способ?

6   51  

6 ответов:

чтобы повторно вызвать исключение и изменить сообщение, сохраняя класс исключения и его обратную трассировку, просто выполните:

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 the rescueD ошибка с анонимным модулем, который расширяет ошибку 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).

Я ставлю свой голос, что Райан Heneise это ответ должен быть принят.

это распространенная проблема в сложных приложениях и сохранение исходного backtrace часто имеет решающее значение настолько, что у нас есть метод полезности в нашем ErrorHandling вспомогательный модуль для этого.

одна из проблем, которую мы обнаружили, заключалась в том, что иногда попытка генерировать более значимые сообщения, когда система находится в перепутанном состоянии, приведет к созданию исключений внутри самого обработчика исключений, который привел нас к упрочнению нашей функции полезности следующим образом:

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>'

вот что я делал:

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!>"

это похоже на Марк Rushakoff'ы ответ, но:

  1. переопределяет to_s вместо message так как по умолчанию message определяется как просто to_s (по крайней мере в Ruby 2.0 и 2.2, где я проверял)
  2. звонки extend для вас вместо того, чтобы заставить абонента сделать этот дополнительный шаг.
  3. использует define_method и закрытие так, что локальная переменная message можно ссылаться. Когда я попытался использовать класс variable @@message, он предупредил: "предупреждение: доступ к переменной класса с верхнего уровня" (см. Это вопрос: "поскольку вы не создаете класс с ключевым словом class, ваша переменная class устанавливается на Object, а не [ваш анонимный модуль]")

характеристики:

  • прост в использовании
  • использует один и тот же объект (вместо создания нового экземпляр класса), поэтому такие вещи, как идентификатор объекта, класс и backtrace сохраняются
  • to_s,message и inspect все отвечают соответствующим образом
  • может использоваться с исключением, которое уже хранится в переменной; не требует, чтобы вы повторно поднимали что-либо (например, решение, которое включало передачу backtrace для повышения: raise $!, …, $!.backtrace). Это было важно для меня, так как исключение было передано в мой метод ведения журнала, а не то, что я спас себя.