0

I have this code that can trigger in production the "Non local exit detected!" branch. I can't understand how that can happen, since even a return will trigger a NonLocalExit exception. Even a throw will trigger an exception.

Is there any way to have exception_raised and yield_returned both false?

def transaction
  yield_returned = exception_raised = nil

  begin
    if block_given?
      result = yield
      yield_returned = true
      puts 'yield returned!'
      result
    end

    rescue Exception => exc
      exception_raised = exc
    ensure
      if block_given?
        unless yield_returned or exception_raised
          puts 'Non local exit detected!'
        end
      end
  end

end


transaction do
  puts 'good block!'
end

transaction do 
  puts 'starting transaction with block with return'
  return 
  puts 'this will not show'
end

Output:

good block!
yield returned!
starting transaction with block with return

I want to somehow output 'Non local exit detected!'. I know this happens in production, but I can't make it happen in development. Tried it with a return and with a throw, but they both raise an exception. Any other way?

Costi
  • 1,231
  • 2
  • 11
  • 18

3 Answers3

1

The problem was with my reproduction, and returning from the top-most level in Ruby. You can return from a method, where there's a stack, but if you return from Kernel, you'll get a LocalJumpError.

Assuming the previous method transaction(). In matters in what context you call return:

def lol
  transaction do
    puts 'Block in method: starting transaction with block with return'
    return 
    puts 'this will not show'
  end
end


lol()

transaction do
  puts 'block in Kernel: starting transaction with block with return'
  return 
  puts 'this will not show'
end

Output:

$ ruby local_jump_error.rb
# Running from method:
Block in method: starting transaction with block with return
yield_returned=nil, exception_raised=nil
Non local exit detected!
# Running without method:
block in Kernel: starting transaction with block with return
yield_returned=nil, exception_raised=#<LocalJumpError: unexpected return> 
local_jump_error.rb:45: unexpected return (LocalJumpError)
            from local_jump_error.rb:6:in `transaction'
        from local_jump_error.rb:43
Costi
  • 1,231
  • 2
  • 11
  • 18
0

I don't know your level with Ruby, but to exit from a block you must use break instead of return. In this case, break also accepts a value in the same way that return does, meaning that a break will conceptually assign the variable result with the value from break.

In case of a throw, it will raise an exception, so it will always unwind the call stack until if finds a rescue statement, thus it will run the code exception_raised = exc.

You can fine tune the rescue to use LocalJumpError instead of Exception to only catch blocks that have return in them. All other exception types will then not stop at that rescue.

Silver Phoenix
  • 522
  • 1
  • 5
  • 10
  • This is legacy code that I'm trying to understand. I need to understand how it can ever reach to 'Non local exit detected!' – Costi Apr 07 '16 at 19:04
  • Well, the only thing that occurs to me right now is with a `callcc`, which seems very farfetched. Are you able to change the code? Add a parameter `&block` and check the origin inside the ensure block with `block.source_location`. – Silver Phoenix Apr 07 '16 at 19:15
0

I wonder if you meant to write:

if block_given?
  if yield_returned.nil? && exception_raised
    puts 'Non local exit detected!'
  end
end

If you make this change, the code will produce the 'Non local exit detected!', for the second method call of #transaction with the return.

When you wrote unless yield_returned or expection_raised, the if clause would get evaluated only when both the variables would have been false. And as I understand that is not possible.

And as a side note, as was suggested in the other answer, one should not rescue Exception, LocalJumpError should be enough.

Community
  • 1
  • 1
Laura Paakkinen
  • 1,661
  • 14
  • 22
  • The mystery about this code is that as it is is hitting 'Non local exit detected!' If I change the code to make it hit that in some other way, I'm not solving the mystery. – Costi Apr 07 '16 at 19:02
  • Ah, now I understand. This seems like a tricky case. The only thing that comes into my mind, is that maybe something else than that return is rescued? If the yielded block would set something to be evaluated in the background, return normally and then just when we have assigned `yield_returned = true`, bam, the background job throws an exception and that gets rescued. Although the changes for this are pretty small... – Laura Paakkinen Apr 07 '16 at 20:03