Skip to content

Instantly share code, notes, and snippets.

@sudoremo
Last active December 12, 2016 13:48
Show Gist options
  • Save sudoremo/8423f16efa2d7ca8943b2b5841ffa5ee to your computer and use it in GitHub Desktop.
Save sudoremo/8423f16efa2d7ca8943b2b5841ffa5ee to your computer and use it in GitHub Desktop.
Calling procs or lambdas can be dangerous when they contain a `return` statement. This mini-library deals with this issue in two different ways.
module ReturnSafeYield
class UnexpectedReturnException < StandardError; end
# Yields the given block and raises a `UnexpectedReturnException` exception if
# the block contained a `return` statement. Thus it is safe to assume that
# yielding a block in this way never jumps out of your surrounding routine.
#
# Note that you cannot pass a block using `safe_yield do`, as it does not make
# sense to check for `return` statements in code controlled by the caller
# itself.
#
# Example:
#
# unknown_block = proc do |some_argument|
# return
# end
#
# ReturnSafeYield.safe_yield(unknown_block, some_argument)
# # => Raises a UnexpectedReturnException exception
def self.safe_yield(block, *args, &cb)
state = :returned
result = block.call(*args, &cb)
state = :regular
return result
rescue
state = :exception
fail
ensure
if state == :returned
fail UnexpectedReturnException, "Block #{block.inspect} contains a `return` which it is not supposed to."
end
end
# Calls the two given blocks (`first`, then `&_second`), even if the first
# block contains a return. The second block receives the return value of the
# first block as arguments.
#
# The second block is not called if the first one raises an exception.
#
# Example:
#
# unknown_block = proc do
# return
# end
# ReturnSafeYield.call_then_yield(unknown_block) do
# # => This line is called even though the above block contains a `return`.
# end
#
# # => This line here might not be called however as the `return` statement
# # exits the current method context.
def self.call_then_yield(first, &_second)
exception = false
first_block_result = nil
begin
first_block_result = first.call
rescue
exception = true
fail
ensure
yield(*first_block_result) unless exception
end
end
end
@am-kantox
Copy link

def self.safe_yield(block, *args, &cb)
    state = :returned
    block.call(*args, &cb).tap { state = :regular }
  rescue
    state = :exception
    fail
  ensure
    fail UnexpectedReturnException, "Block #{block.inspect} contains a `return` which it is not supposed to." if state == :returned
  end
end

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment