I have condensed my problem to what I believe is a minimum reproducible case:
class AbortReading < RuntimeError; end
class SomeError < RuntimeError; end
def rno
retval = false
catch(:abort_reading) do
begin
yield
rescue AbortReading
puts "throw abort_reading"
throw :abort_reading
end # begin
puts "Setting to true"
retval = true
end # catch
ensure # rno
puts "rno returns #{retval.inspect}"
retval # return value
end
def rfb
success = rno do
begin
puts "failing"
fail SomeError
rescue SomeError
puts "intercepted SomeError"
fail AbortReading
end
end
puts "success=#{success.inspect}"
success
end
puts rfb
I have two methods, rno
and rfb
. rno
is supposed to take a block. It returns true, unless the block raises the exception AbortReading
, in which case it returns false. Note the somewhat unusual usage of throw
to jump prematurely to the end of rno
; this construct is taken from the actual (more complex) code, where it does make sense, and I also used it in my example case, since i feel that the cause of the problem could be in this part.
The method rfb
uses rno
, and in its body it first raises a SomeError
and turns this exception into a AbortReading
. This somewhat odd construct is also taken from the original implementation.
I would expect that the invocation of rfb
would result into false
, since it causes a AbortReading
, and rno
would then return then false
from it. However, rfb
returns nil
. This means that the variable success
inside rfb
has been allocated, but it never receives the value of retval
.
Running the code produces the output
failing
intercepted SomeError
throw abort_reading
rno returns false
success=nil
Note in particular, that rno
does return false just before it terminates, but inside rfb
, the value is nil. What's going on here?
CodePudding user response:
Right now the return value from rno
is actually the result of the catch
block, which is nil
because you called throw :abort_reading
without supplying a return value.
The ensure
keyword does not implicitly return it just "ensures" this code runs before the method returns as it normally would.
If you want ensure
to return you would need to do so explicitly using the return
keyword. e.g.
def rno
retval = false
catch(:abort_reading) do
begin
yield
rescue AbortReading
puts "throw abort_reading"
throw :abort_reading
end # begin
puts "Setting to true"
retval = true
end # catch
ensure # rno
puts "rno returns #{retval.inspect}"
return retval # return value
end
That being said I would not recommend this and rather I would use the fact that you can provide a return value with Kernel#throw
so we can refactor your code to
def rno
retval = catch(:abort_reading) do
begin
yield
puts "Setting to true"
true
rescue AbortReading
puts "throw abort_reading"
throw :abort_reading, false
end # begin
end # catch
ensure # rno
puts "rno returns #{retval.inspect}"
end
Here, in the event of an AbortReading
error, we are throwing the symbol :abort_reading
along with the return value false
so the result of the catch
block will be false
when it catches :abort_reading
or true
if the yield
does not result in an AbortReading
error.
Now the output of calling puts rfb
is
failing
intercepted SomeError
throw abort_reading
rno returns false
success=false
false