Как добавить информацию в сообщение об исключении в Ruby? - PullRequest
54 голосов
/ 13 мая 2010

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

Подход, который я сейчас использую:

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

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

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

Ответы [ 7 ]

93 голосов
/ 25 января 2011

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

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...
17 голосов
/ 13 мая 2010

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

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

Вы также можете получить модифицированный объект исключения самостоятельно с помощью метода exception:

new_exception = $!.exception "Problem with string number #{i}: #{$!}"
raise new_exception
6 голосов
/ 01 ноября 2012

Вот еще один способ:

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)

4 голосов
/ 13 мая 2010

Мой подход заключается в 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).

2 голосов
/ 30 мая 2019

Я понимаю, что опоздал на эту вечеринку на 6 лет, но ... Я думал, что до этой недели понял обработку ошибок в Ruby и наткнулся на этот вопрос. Хотя ответы полезны, существует неочевидное (и недокументированное) поведение, которое может быть полезным для будущих читателей этой темы. Весь код был запущен под ruby ​​v2.3.1.

@ Эндрю Гримм спрашивает

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

и затем предоставляет пример кода:

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

Я думаю, крайне важно указать, что это НЕ добавляет информацию к исходному объекту экземпляра ошибки , но вместо этого вызывает НОВЫЙ объект ошибки с тем же классом.

@ BoosterStage говорит

Чтобы пересмотреть исключение и изменить сообщение ...

но опять же предоставленный код

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

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

Разница между кодом @Andrew Grimm и примером @ BoosterStage заключается в том, что первый аргумент #raise в первом случае равен Class, тогда как во втором случае он является экземпляром некоторого (предположительно) StandardError. Разница имеет значение, потому что документация для Kernel # повышение говорит:

С одним аргументом String вызывает RuntimeError со строкой в ​​качестве сообщения. В противном случае первым параметром должно быть имя класса исключения (или объекта, который возвращает объект исключения при отправке сообщения об исключении).

Если указан только один аргумент и это экземпляр объекта ошибки, этот объект будет raise d IF , метод этого объекта #exception наследует или реализует поведение по умолчанию определено в Исключение # исключение (строка) :

Без аргумента или, если аргумент совпадает с получателем, вернуть получателя. В противном случае создайте новый объект исключения того же класса, что и получатель, но с сообщением, равным 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

Итак, TestError был initialize d, rescue d, и его сообщение было напечатано. Все идет нормально. Второй тест (аналог примера @ BoosterStage, приведенного выше):

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

Несколько удивительные результаты:

initialize
exception
bar

Таким образом, TestError был initialize d с 'foo', но затем #raise вызвал #exception для первого аргумента (экземпляр TestError) и передал в сообщении 'bar' для создания второго экземпляра TestError, что в конечном итоге поднимается .

TIL.

Также, как и @Sim, я очень обеспокоен сохранением любого исходного контекста возврата, но вместо реализации пользовательского обработчика ошибок, такого как его raise_with_new_message, Ruby's Exception#cause получает мою поддержку: всякий раз, когда Я хочу перехватить ошибку, обернуть ее в ошибку, относящуюся к конкретному домену, а затем вызвать эту ошибку . У меня все еще есть исходная обратная трассировка, доступная через #cause при возникновении ошибки, относящейся к конкретному домену.

Смысл всего этого в том, что - как @Andrew Grimm - я хочу поднимать ошибки с большим контекстом; в частности, я хочу, чтобы в определенных случаях в моем приложении возникали специфичные для домена ошибки, которые могут иметь множество режимов сбоя, связанных с сетью. Затем мои отчеты об ошибках могут быть обработаны для ошибок домена на верхнем уровне моего приложения, и у меня есть весь контекст, который мне нужен для регистрации / отчетности, рекурсивно вызывая #cause, пока я не доберусь до «первопричины».

Я использую что-то вроде этого:

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

Теперь контроллеру Rails, вызывающему ServiceX, не нужно особенно знать, что ServiceX использует Фарадей (или gRPC, или что-то еще), он просто делает вызов и обрабатывает BaseDomainError. Опять же: для целей ведения журнала один обработчик на верхнем уровне может рекурсивно регистрировать все #cause любых обнаруженных ошибок, а для любых BaseDomainError экземпляров в цепочке ошибок он также может регистрировать значения extra, потенциально включая локальные переменные извлекаются из инкапсулированных binding (s).

Надеюсь, этот тур был таким же полезным для других, как и для меня. Я многому научился.

ОБНОВЛЕНИЕ: Skiptrace похоже, что оно добавляет привязки к ошибкам Ruby.

Также см. этот другой пост для получения информации о том, как реализация Exception#exception будет клонировать объект (копирование переменных экземпляра).

2 голосов
/ 31 августа 2013

Я поставил свой голос, чтобы ответ Райана Хениза был принятым.

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

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

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>'
0 голосов
/ 09 мая 2015

Вот что я в итоге сделал:

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, он предупреждал: «предупреждение: доступ к переменной класса из верхнего уровня» (см. вопрос : «Поскольку вы не создаете класс с ключевым словом class, ваша переменная класса Object, а не [ваш анонимный модуль] ")

Особенности:

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