edmz

View on GitHub

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
end

In 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!
end

You can check the a full working example in this gist.