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.
>Solution :
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:
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,putsreturnsnil- script would have exit if
exitwasn’t commented out - as there is no
return,stop_scriptreturns the last statement’s value, which isnil throwis finally called asthrow(:ctrl_c, nil)catchexits with valuenil
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.