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
exitfor now, so we can see the full execution:What actually happens:
catchexecutes its blocktrapsets aSIGINThandlereachstarts executing, and is interrupted by the keyboard interruptSIGINThandler executesthrow(:ctrl_c, stop_script)executes; sincethrowis a method, its arguments need to be evaluated before the method callstop_scriptin Ruby is equivalent toself.stop_script()if such a method exists, sostop_scriptis executed"CTRL_C seen"is printed,putsreturnsnilexitwasn't commented outreturn,stop_scriptreturns the last statement's value, which isnilthrowis finally called asthrow(:ctrl_c, nil)catchexits with valuenilYour fourth example is equivalent, only missing the first and last step.
Crucially, the second parameter to
throwis only the value that will be passed tocatchas its return value. It has no magic "evaluate this later" power: it evaluates beforethrowis called, just like any argument to a method.If a
throwis not caught bycatchby the time the script reaches its natural end, you will get an exception printed. However, in your last example, given thatexitis executed, the script exits then and there, the exception situation does not occur.