Bug #19598
openInconsistent behaviour of TracePoint API
Description
Hello,
I am seeing inconsistent behaviour of the TracePoint API. If I raise an error from within the :raise
event block it crashes the entire program with a exception reentered (fatal)
next time any error is raised. However if I add a simple if
check in the :raised
event block the same program doesn't crash anymore.
My specific use case is that sometimes when I have Exception
s being raised in my application they are being handled by ActiveRecord and wrapped in a ActiveRecord::StatementInvalid
, which is a StandardError
. The codebase has a lot of rescue StandardError
statements which swallow the StatementInvalid
and therefore the Exception
s get ignored. I would like to bypass the rescue StandardError
statements in this case. My current solution is to manually check in every rescue StandardError
if the StatementInvalid
has an Exception
in its .cause
attribute and if there is re-raise it, but the codebase is very big and this is not a very good solution as every developer needs to remember to do this check if they add a new rescue StandardError
or modify an existing one.
Using TracePoint to do the aforementioned check before any rescue
statements are called and then re-raise the Exception seems like a very neat way to automate the handling of these masked Exception
s. However I am getting inconsistent behaviour from Ruby depending on what code I put inside the :raised
event handler. Here are two identical pieces of code apart from an extra if
check in the second example. The first example crashes with exception reentered (fatal)
, the second doesn't.
Code to reproduce crash¶
require "active_record"
class Test
def run
begin
tp = TracePoint.new(:raise) do |t|
puts "TracePoint received: #{t.raised_exception.class}"
raise t.raised_exception.cause
end
puts "TracePoint created"
tp.enable do
puts "TracePoint enabled"
# Generate an Exception masked as a StatementInvalid
begin
raise Exception
catch Exception
raise ActiveRecord::StatementInvalid
end
end
rescue Exception => e
puts "Got Exception instead of StatementInvalid"
end
end
end
t = Test.new
t.run
begin
raise ArgumentError
rescue ArgumentError => e
puts "Never reach here"
end
Output¶
TracePoint created
TracePoint enabled
TracePoint received: Exception
Got Exception instead of StatementInvalid
tp_test2.rb: exception reentered (fatal)
Code that doesn't crash, extra if check on line 8¶
require "active_record"
class Test
def run
begin
tp = TracePoint.new(:raise) do |t|
puts "TracePoint received: #{t.raised_exception.class}"
if t.raised_exception.instance_of?(ActiveRecord::StatementInvalid)
raise t.raised_exception.cause
end
end
puts "TracePoint created"
tp.enable do
puts "TracePoint enabled"
# Generate an Exception masked as a StatementInvalid
begin
raise Exception
catch Exception
raise ActiveRecord::StatementInvalid
end
end
rescue Exception => e
puts "Got Exception instead of StatementInvalid"
end
end
end
t = Test.new
t.run
begin
raise ArgumentError
rescue ArgumentError => e
puts "Never reach here"
end
Output¶
TracePoint created
TracePoint enabled
TracePoint received: Exception
Got Exception instead of StatementInvalid
Never reach here
Updated by ko1 (Koichi Sasada) over 1 year ago
(1) More simple reproducible code is very welcome because it is easy to understand the situation.
(2) catch Exception
=> rescue Exception
off topic:
I wonder it is valid code:
begin
raise 'foo'
catch => e
p e
end
because it is parsed as
begin
raise 'foo'
catch() => e # 1 line pattern match but not reached because of `raise`
p e
end
Updated by alanwu (Alan Wu) over 1 year ago
Side note, Kernel#catch
without a block doesn't seem to make sense.
Maybe it should raise "no block given" like Kernel#tap
.
Updated by bgdimitrov (Bogdan Dimitrov) over 1 year ago
Thank you, the catch
instead of rescue
was causing the inconsistency, changing that makes both examples fail with exception reentered (fatal)
.
Is this behaviour expected though? It looks like after raising an error from within the TracePoint the whole error-raising mechanism is broken, other code runs ok (e.g. the puts
statement on line 24) but when we try to raise
again we get the exception reentered (fatal)
.
I have slimmed down the example to this:
$tp = TracePoint.new(:raise) do |t|
puts "TracePoint received #{t.raised_exception.class}"
puts "TracePoint raising ArgumentError"
raise ArgumentError
end
class Test
def run
$tp.enable do
begin
puts "Raising NameError"
raise NameError
rescue ArgumentError
puts "Handled TracePoint's ArgumentError"
end
end
end
end
t = Test.new
t.run
begin
puts "Raising follow-up error"
raise NotImplementedError
rescue NotImplementedError => e
puts "Never reach here"
end
Output:
Raising NameError
TracePoint received NameError
TracePoint raising ArgumentError
Handled TracePoint's ArgumentError
Raising follow-up error
tp_test3.rb: exception reentered (fatal)