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
