6

I have built a simple banking application, which is able to perform the usual operations; Deposit, Withdraw etc.

My controller methods perform these operations and rescue exceptions that are raised by the account or other entities.

Here are some of these methods used in the controller code:

def open(type, with:)
    account = create type, (holders.find with)
    add account
    init_yearly_interest_for account
    boundary.render AccountSuccessMessage.new(account)
  rescue ItemExistError => message
    boundary.render message
  end

  def deposit(amount, into:)
    account = find into
    account.deposit amount
    boundary.render DepositSuccessMessage.new(amount)
  rescue ItemExistError => message
    boundary.render message
  end

  def withdraw(amount, from:)
    account = find from
    init_limit_reset_for account unless account.breached?
    account.withdraw amount
    boundary.render WithdrawSuccessMessage.new(amount)
  rescue ItemExistError, OverLimit, InsufficientFunds => message
    boundary.render message
  end

  def get_balance_of(id)
    account = find id
    boundary.render BalanceMessage.new(account)
  rescue ItemExistError => message
    boundary.render message
  end

  def transfer(amount, from:, to:)
    donar = find from
    recipitent = find to
    init_limit_reset_for donar unless donar.breached?
    donar.withdraw amount
    recipitent.deposit amount
    boundary.render TransferSuccessMessage.new(amount)
  rescue ItemExistError, OverLimit, InsufficientFunds => message
    boundary.render message
  end

  def add_holder(id, to:)
    holder = holders.find id
    account = find to
    account.add_holder holder
    boundary.render AddHolderSuccessMessage.new(holder, account)
  rescue ItemExistError, HolderOnAccount => message
    boundary.render message
  end

  def get_transactions_of(id)
    transactions = (find id).transactions
    boundary.render TransactionsMessage.new(transactions)
  rescue ItemExistError => message
    boundary.render message
  end

  def get_accounts_of(id)
    holder = holders.find id
    accounts = store.select { |_, a| a.holder? holder }.values
    boundary.render DisplayAccountsMessage.new(accounts)
  rescue ItemExistError => message
    boundary.render message
  end

As you can see I am rescuing multiple errors during multiple methods, often the same errors are handled.

Although this is working, I wonder if it is possible to refactor and handle these exceptions whenever any of the methods in the controller are called.

So for example:

during: 
  open, deposit, withdraw
rescue ItemExistError => message
  boundary.render message

Any help would be greatly appreciated.

Phil Brockwell
  • 456
  • 5
  • 22

4 Answers4

10

You can do this with metaprogramming by defining a method that wraps each of the methods you want to rescue from. It's your call whether or not this is actually cleaner code.

class MyController
  # define a unified exception handler for some methods
  def self.rescue_from *meths, exception, &handler
    meths.each do |meth|
      # store the previous implementation
      old = instance_method(meth)
      # wrap it
      define_method(meth) do |*args|
        begin
          old.bind(self).call(*args)
        rescue exception => e
          handler.call(e)
        end
      end
    end
  end

  rescue_from :open, :deposit, :withdraw, ItemExistError do |message|
    boundary.render message
  end
end

If you aren't going to reuse the method (i.e. if you only want a unified handler for this one set of methods and this one exception class), I would remove the rescue_from definition and put the metaprogramming code right in the class.

Max
  • 21,123
  • 5
  • 49
  • 71
  • is it good practice to put the rescue_from call in private? Does it matter? – toobulkeh Mar 03 '17 at 19:56
  • I would call that good practice. Realistically you probably don't want code outside of the class to be adding new `rescue_from` cases. – Max Mar 04 '17 at 19:54
  • 1
    Ruby 2.7+ adds UnboundMethod#bind_call method, so you can use `old.bind_call(self, *args)` instead https://ruby-doc.org/core-2.7.0/UnboundMethod.html#method-i-bind_call – Iggy Dec 15 '22 at 17:03
8

You could try writing a method like this:

def call_and_rescue
  yield if block_given?
rescue ItemExistError => message
  boundary.render message
end

Then use it: call_and_rescue { open(type, with) }

Piotr Kruczek
  • 2,384
  • 11
  • 18
  • Unfortunately not a rails app, but I was hoping for something similar – Phil Brockwell Jul 13 '15 at 15:53
  • Doesn't this just move the rescue code from being repeated in each method definition to being repeated at every method call? That doesn't seem like an improvement to me. – Max Jul 13 '15 at 16:46
  • @PhilBrockwell I think this is all pure ruby, no? https://ruby-doc.org/core-2.4.1/Kernel.html#method-i-block_given-3F – dgmstuart Nov 06 '17 at 12:54
  • 1
    @max For me this is an improvement, since it de-duplicates the knowledge of what error is being rescued and how. Consider what would happen if the specific error being rescued changed, or additional errors needed to be rescued: those changes could now happen in one place instead one change per method. – dgmstuart Nov 06 '17 at 12:58
2

Rails' ActiveSupport gem provides the rescue_from helper:

https://edgeapi.rubyonrails.org/classes/ActiveSupport/Rescuable/ClassMethods.html#method-i-rescue_from

class MyController < ApplicationController
  rescue_from ItemExistError, with: :item_exist_error

  def get_transactions_of(id)
    transactions = (find id).transactions
    boundary.render TransactionsMessage.new(transactions)
  end


  def get_accounts_of(id)
    holder = holders.find id
    accounts = store.select { |_, a| a.holder? holder }.values
    boundary.render DisplayAccountsMessage.new(accounts)
  end

  private

  def item_exist_error(_exception)
    boundary.render message
  end
end
eikes
  • 4,811
  • 2
  • 31
  • 31
0

Would a before and after style callback help?

Variables to store the list of actions that have callbacks

@before_actions = Hash.new {|hash,key| hash[key] = Array.new}
@after_actions = Hash.new {|hash,key| hash[key] = Array.new}

Defining the before_action method action is the callback to be executed, methods is an array of actions that have callbacks assigned to them

def before_action(action, methods)  
  methods.each do |m|
    @before_actions[m] << action
  end
end

def after_action(action, methods)  
  methods.each do |m|
    @after_actions[m] << action
  end
end

Block used inside our methods that allows us to use these callbacks

def execute_callbacks  
  @before_actions.each {|k,v| v.each {|v| send(v)}}
  yield
  @after_actions.each {|k,v| v.each {|v| send(v)}}
end

Declaring the before and after actions that we want to occur (maybe put callbacks like #first_do_this and #lastly_do_this as private methods?)

before_action :first_do_this, [:do_something]  
after_action :lastly_do_this, [:do_something]

private
def first_do_this  
  puts "this occurs first"
end

def lastly_do_this  
  puts "this occurs last"
end

Our method that we want to work around. (I know its not that elegant, it could be improved)

def do_something  
  execute_callbacks do
    puts "hello world"
  end
end

When do_something is called

do_something # =>
  this occurs first
  hello world
  this occurs last
pyRabbit
  • 803
  • 1
  • 9
  • 33
  • I think this is along the right lines, but Im not sure it would work as rescue needs to listen for exceptions whist the code is running, not before or after. Although maybe Ive misuderstood your solution... – Phil Brockwell Jul 13 '15 at 16:07
  • I haven't tried anything like this in production. It was more of just playing around and you may have a very valid point. It may be worth a shot and if it doesn't work at least we tried and I am sure someone here can help! – pyRabbit Jul 13 '15 at 16:14
  • I honestly like @Piotr Kruczek's solution, does something like that work for you? – pyRabbit Jul 13 '15 at 16:17
  • It works but because I'm passing a block to the controller it means I have to write more code to evaluate methods before they go into that block in my boundary class.... – Phil Brockwell Jul 13 '15 at 16:27