Did you know that calling return in one Ruby method could affect the flow of another method? I discovered it today while hunting a GraphQL-Ruby bugfix. You can get more reliable behavior with ensure, if it’s appropriate.
Instrumentating a block
Let’s imagine a simple instrumentation system, where method wraps a block of code and tags it with a name:
def instrument_event(event_name)
puts "begin #{event_name}"
result = yield
puts "end #{event_name}"
result
end
You could use this to instrument a method call, for example:
def do_stuff_with_instrumentation
instrument_event("do-stuff") do
do_stuff
end
end
do_stuff_with_instrumentation
# begin do-stuff
# end do-stuff
It prints the begin message, then the end message.
Returning early
But what if you return early from the block? For example:
# @param return_early [Boolean] if true, return before actually doing the stuff
def do_stuff_with_instrumentation(return_early:)
instrument_event("do-stuff") do
if return_early
# Return from this method ... but also return from the `do ... end` instrumentation block
return
else
do_stuff
end
end
end
If you instrument it without returning from inside the block, it logs normally:
do_stuff_with_instrumentation(return_early: false)
# begin do-stuff
# end do-stuff
But, if you return early, you only get half the log:
do_stuff_with_instrumentation(return_early: true)
# begin do-stuff
Where’s the end message?
It Jumped!
Apparently, the return inside the inner method (#do_stuff_with_instrumentation) broke out of its own method and out of #instrument_event. I don’t know why it works like that.
With Ensure
If you refactor the instrumentation to use ensure, it won’t have this issue. Here’s the refactor:
def instrument_event(event_name)
puts "begin #{event_name}"
yield
ensure
puts "end #{event_name}"
end
Then, it prints normally:
do_stuff_with_instrumentation(return_early: true)
# begin do-stuff
# end do-stuff
Of course, this also changes the behavior of the method when errors happen. The ensure code will be called even if yield raises an error. So, it might not always be the right choice. (I bet you could use $! to detect a currently-raised error, though.)