Using catch-throw for control flow in Ruby
Imagine this scenario. You have to pay for a digital good and you have an array of payment methods (credit card, cash, whatever).
You can pay partially with any of the payment methods. That is, you can pay $10 on your credit card, $5 on cash, etc.
If any of the payment methods fails or if the attempt to acquire the digital good fails or if the capture of the money fails, you have to rollback everything you’ve done so far.
This has the potential of quickly turning into a spaghett-if hell.
if money_was_captured
result = acquire_goods
if result.success?
result = capture_money
if result.success?
build_receipt
end
else
rollback
end
else
rollback
end
#..Too many levels, too many ifs.
There is an easy way to avoid this behaviour by taking advantage of catch-throw as a way to control flow.
By using catch, we could concentrate on specifying our desired
workflow and then acting in case the flow was not executed completely.
We take advantage of the fact that throw has the option of
returning execution to its matching catch and returning
a value we specified.
# this will always return :yes
result = catch(:purchase_error) do
throw(:purchase_error, :yes)
:no
endIn this excerpt PaymentPlan and Digital good throw a :purchase_error
when they were not able to complete their transaction.
pp = PaymentPlan.new
result = catch(:purchase_error) do
pp.hold!
DigitalGood.new.acquire
pp.capture!
pp
end
if [:capture_failed, :purchase_failed].include? result
pp.rollback!
endYou can check the a full working example in this gist.