Я понимаю, что опоздал на эту вечеринку на 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
будет клонировать объект (копирование переменных экземпляра).