I noticed this behavior while playing with trap and with catch/throw methods to better understand how they can/should be used.
In Ruby the catch and throw methods appear to be intended to be used in pairs:
catch(:ctrl_c) do
trap("SIGINT") { throw :ctrl_c }
(1.. ).each {|n| print "."; sleep 0.5 }
end
# SIGINT trapped -> throw called -> catch block exits
# ruby covthrow1p.rb
# => ........
Or with a second parameter provided to the throw method:
def stop_script
puts 'CTRL_C seen'
exit
end
catch(:ctrl_c) do
trap("SIGINT") { throw :ctrl_c, stop_script }
(1.. ).each {|n| print "."; sleep 0.5 }
end
# SIGINT trapped -> throw called -> catch executes stop_script & exits block
# ruby covthrow2p.rb
# => ......CTRL_C seen
If throw is used alone (naked) with only the key parameter provided, it fails:
def stop_script
puts 'CTRL_C seen'
exit
end
trap("SIGINT") { throw :ctrl_c }
(1.. ).each {|n| print "."; sleep 0.5 }
# SIGINT trapped -> throw called -> No catch block -> UncaughtThrowError
# ruby nkdthrow1p.rb
# => .......nkdthrow1p.rb:8:in `throw': uncaught throw :ctrl_c (UncaughtThrowError)
But if a naked throw is used with the second parameter provided, it succeeds!
def stop_script
puts 'CTRL_C seen'
exit
end
trap("SIGINT") { throw :ctrl_c, stop_script }
(1.. ).each {|n| print "."; sleep 0.5 }
# SIGINT trapped -> throw called -> No catch block -> call stop_script ???
# ruby nkdthrow2p.rb
# => ......CTRL_C seen
Is this intended behavior? An artifact of implementation? A bug? Seems harmless generally but could cause confusing behavior.
The examples are on Ruby 3.2.2 running on Windows 10.
Your interpretation of the second example is wrong. Let's comment out exit
for now, so we can see the full execution:
def stop_script
puts 'CTRL_C seen'
# exit
end
catch(:ctrl_c) do
trap("SIGINT") { throw :ctrl_c, stop_script }
(1.. ).each {|n| print "."; sleep 0.5 }
end
What actually happens:
catch
executes its blocktrap
sets a SIGINT
handlereach
starts executing, and is interrupted by the keyboard interruptSIGINT
handler executesthrow(:ctrl_c, stop_script)
executes; since throw
is a method, its arguments need to be evaluated before the method callstop_script
in Ruby is equivalent to self.stop_script()
if such a method exists, so stop_script
is executed"CTRL_C seen"
is printed, puts
returns nil
exit
wasn't commented outreturn
, stop_script
returns the last statement's value, which is nil
throw
is finally called as throw(:ctrl_c, nil)
catch
exits with value nil
Your fourth example is equivalent, only missing the first and last step.
Crucially, the second parameter to throw
is only the value that will be passed to catch
as its return value. It has no magic "evaluate this later" power: it evaluates before throw
is called, just like any argument to a method.
If a throw
is not caught by catch
by the time the script reaches its natural end, you will get an exception printed. However, in your last example, given that exit
is executed, the script exits then and there, the exception situation does not occur.