Home > database >  Ruby value myseteriously gets lost during exception handling
Ruby value myseteriously gets lost during exception handling

Time:11-22

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
  •  Tags:  
  • ruby
  • Related