2

I have a method that's nested within a couple other methods, and I want this method to break from all recursive methods with a returned error. For instance, if I have pay_order being called here:

class API::StripeController < ApiController
  def my_api_action
    # ...
    order = create_order
    pay_order(order, token)
    # do things if payment is successful
  end
end

And pay_order is defined as:

def pay_order(order, token)
  begin
    order.pay(source: token)
  rescue Stripe::CardError => e
    break e
  end
end

How do I break out of all parent methods and return the card error coming from the failed payment? I need something like break and return e or return e and break. But I know both the break and return statements immediately return, so I can't chain them (I don't think).

I could just add return statements to each function being called, but I plan on using this method in lots of places, and don't plan on ever needing it to behave differently, so I'm looking for the most reusable way to write it.

Doug
  • 1,517
  • 3
  • 18
  • 40

2 Answers2

5

Why do you rescue inside the pay_order method? I'd rescue on the outer loop. Given the following:

def method_a
  10.times do |loop_a|
    method_b
  end
end

def method_b
  5.times do |loop_b|
    pay_order
  end
end

def pay_order
  ...
end

I'd rescue inside method_a, for example:

def method_a
  10.times do |loop_a|
    method_b
  end
rescue Stripe::CardError => e
  # do whatever. no need to break
end

All the loops are "broken" automatically by the raising of the exception.

If you want to do something with the exception inside the pay_order method, then I would suggest to rescue and raise again:

def pay_order
  order.pay
rescue Stripe::CardError => e
  # do your stuff
  raise e
end
Jagdeep Singh
  • 4,880
  • 2
  • 17
  • 22
coorasse
  • 5,278
  • 1
  • 34
  • 45
  • _"Why do you rescue inside the pay_order method?"_ - @coorasse i think it is a good idea to handle the payment related exception in the pay method itself. The method `pay_order` can be used from anywhere in the app. – Jagdeep Singh Jul 05 '18 at 06:36
  • I guess I wrote it like that because of an elementary knowledge of error handling. I wanted to find a way to only write the rescue statement once inside the `pay_order` method, and perhaps thought too hard. – Doug Jul 05 '18 at 06:39
  • If you want to localise payment error rescue inside payment method, you can define your own exception to wrap the payment error in, but the mechanism remains the same. – Amadan Jul 05 '18 at 06:53
  • then the second part of my answer is for you. rescue, do your stuff and raise again to break the loops. – coorasse Jul 05 '18 at 06:54
1

Basically you can use throw, or raise (respectively fail, see here for a discussion whether to use raise or fail).

The difference is that with raise/fail, you are producing an exception (which you catch with rescue), and the Exception object can contain as payload the data you want to return.

With throw, you are simply doing kind of a goto upward the call chain. The landing point is defined using catch (see for instance here). This does not have any payload, which means that you need to "transport" any data back using an object which is accessible on both sides (for instance an instance variable).

user1934428
  • 19,864
  • 7
  • 42
  • 87
  • 1
    Good answer. One thing to keep in mind is that generating exceptions in Ruby is an expensive operation in Ruby compared to many other languages, as the "payload" referred to here is Ruby having to generate the entire call-stack and return it. This is why a `rescue next` is typically frowned upon within a loop (among other reasons...). So really it boils down to whether or not you need the exception data or not. If you don't care about the exception, then `throw .. catch` seems like the way to go. In addition, a simple `return` breaks out of a nested loops, but not always possible to do. – ForeverZer0 Jul 05 '18 at 07:42
  • 1
    catch and throw can absolutely carry a payload. throw accepts a second argument, which defaults to nil, as the value to return for the catch block. if nothing is thrown, catch returns the last statement executed as its value – Sampson Crowley Jun 28 '22 at 15:27